diff --git a/docs/apps/linkdin/templates/graph_view_template.html b/docs/apps/linkdin/templates/graph_view_template.html index 9d908d8fc..745bba68e 100644 --- a/docs/apps/linkdin/templates/graph_view_template.html +++ b/docs/apps/linkdin/templates/graph_view_template.html @@ -9,6 +9,7 @@ + @@ -250,6 +251,32 @@

Organization Details

", _html(), re.S) + assert scripts, "no inline ", + "vbscript:msgbox(1)", + ], +) +def test_safe_url_blocks_dangerous_schemes(payload): + """safeUrl() must fall back instead of letting a non-http(s) scheme through. + + This is the gap the issue's collaborator explicitly flagged: entity + escaping alone does not stop a `javascript:` URL from reaching + setAttribute for an href/src sink. + """ + node_src = ( + _extract_escape_html_fn() + + "\n" + + _extract_safe_url_fn() + + f"\nconsole.log(JSON.stringify(safeUrl({payload!r})));\n" + ) + result = subprocess.run( + ["node", "-e", node_src], capture_output=True, text=True, timeout=30 + ) + assert result.returncode == 0, result.stderr + returned = result.stdout.strip() + assert payload not in returned + assert returned == '"#"' + + +@pytest.mark.skipif(not _has_node(), reason="node is required to execute the template's JS") +@pytest.mark.parametrize( + "payload", + [ + "https://www.linkedin.com/in/example", + "http://www.linkedin.com/in/example", + ], +) +def test_safe_url_allows_http_and_https(payload): + node_src = ( + _extract_escape_html_fn() + + "\n" + + _extract_safe_url_fn() + + f"\nconsole.log(JSON.stringify(safeUrl({payload!r})));\n" + ) + result = subprocess.run( + ["node", "-e", node_src], capture_output=True, text=True, timeout=30 + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == f'"{payload}"' + + +# Each entry is a snippet that must be present in the fixed template, taken +# from one of the five sinks the issue reported. Checking out the pre-fix +# source (HEAD~1) for this file makes every one of these assertions fail, +# since escapeHtml() did not exist and these fields were interpolated raw. +EXPECTED_ESCAPED_SNIPPETS = [ + # 1. Company list (was: ${n.name}, ${n.industry ...}, ${n.about ...}, ${n.handle}) + '

${escapeHtml(n.name)}

', + "${escapeHtml(n.industry || 'N/A')}", + "${escapeHtml(n.about || 'No description available')}", + "https://www.linkedin.com${escapeHtml(n.handle || '')}", + "${escapeHtml(n.followers?.toLocaleString() || '0')} followers", + # 2. renderOrg (was: ${companyName}, ${n.name}, ${n.title}, ${n.profile_url}) + '

${escapeHtml(companyName)}

', + '
${escapeHtml(n.name)}
', + '
${escapeHtml(n.title)}
', + '${escapeHtml(p.name)}', + "${escapeHtml(p.title || 'Employee')}", + "${escapeHtml(p.dept || 'Department not specified')}", + "${escapeHtml(p.title_level || 'Level not specified')}", + '${escapeHtml(p.yoe_current || \'?\')} years at company', + '${escapeHtml(p.connection_count || \'?\')} connections', + # 4. AI chat drawer (was: raw text += ..., marked.parse(text) unsanitized) + 'el.lastChild.innerHTML += escapeHtml(text).replace(/\\n/g, "
")', + "contentEl.innerHTML = DOMPurify.sanitize(marked.parse(text))", + # 5. Graph hover tooltip (was: ${node.name}, ${node.industry ...}, ${node.about ...}) + '
${escapeHtml(node.name)}
', + "${escapeHtml(node.industry || 'Industry: N/A')}", + '
${escapeHtml(node.about || \'\')}
', + "${escapeHtml(node.followers?.toLocaleString() || '0')} followers", +] + + +@pytest.mark.parametrize("snippet", EXPECTED_ESCAPED_SNIPPETS) +def test_sink_interpolates_through_escape_html(snippet): + html = _html() + assert snippet in html, f"expected escaped interpolation not found: {snippet!r}" + + +def test_dompurify_is_loaded(): + assert re.search(r'