mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix(javascript): report SVG script references and skipped bodies (#8627)
Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
This commit is contained in:
1 parent
f95bdbce76
commit
20a282a181
4 files changed
+175
-6
No files matched your search
@@ -1002,7 +1002,12 @@ reason in the renderer observations.
|
||||
HTML script source ranges follow the HTML parser across LF, CRLF, and bare CR
|
||||
line endings, preserving UTF-16 columns.
|
||||
Inline JavaScript in local HTML, meaning classic or module `<script>` bodies
|
||||
without a `src` attribute, is not analyzed. Each HTML file that has one gets an
|
||||
without a `src` attribute, is not analyzed. Embedded SVG scripts use `href`
|
||||
(or `xlink:href` when `href` is absent) instead: an existing reference, even
|
||||
an empty one, suppresses the inline body. SVG's `src` and HTML-only `language`
|
||||
attributes do not suppress its JavaScript body. SVG references and inline
|
||||
bodies share the same inert parsing and source coordinates as HTML scripts;
|
||||
template content stays inert. Each HTML file that has an inline body gets an
|
||||
unknown `parse-html-inline-scripts` scope with the exact body count and the
|
||||
script type and source range of the first 100 bodies; beyond that, coverage is
|
||||
truncated under the `html_inline_script_locations_per_file` limit, and the
|
||||
|
||||
@@ -106,7 +106,7 @@ it.each([
|
||||
['<script language="vbscript">run()</script>', []],
|
||||
["<!-- <script>run();</script> -->", []],
|
||||
["<template><script>run();</script></template>", []],
|
||||
["<svg><script>run();</script></svg>", []],
|
||||
["<svg><script>run();</script></svg>", ["classic"]],
|
||||
])("classifies inline JavaScript in %s", (text, expected) => {
|
||||
expect(
|
||||
htmlArtifactReferences(text).inlineScripts.map(
|
||||
@@ -115,6 +115,65 @@ it.each([
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['src="ignored.js"', "classic"],
|
||||
['language="vbscript"', "classic"],
|
||||
['xlink:type="simple"', "classic"],
|
||||
['type=" Text/JavaScript "', "classic"],
|
||||
['type="MODULE"', "module"],
|
||||
['type=" module "', null],
|
||||
['type="application/json"', null],
|
||||
['type="text/javascript; charset=utf-8"', null],
|
||||
['href="external.js"', null],
|
||||
['href=""', null],
|
||||
['xlink:href="external.js"', null],
|
||||
] as const)("classifies SVG inline bodies using %s", (attributes, expected) => {
|
||||
const result = htmlArtifactReferences(
|
||||
`<svg><script ${attributes}>run();</script></svg>`,
|
||||
);
|
||||
expect(result.inlineScripts.map(({ scriptType }) => scriptType)).toEqual(
|
||||
expected === null ? [] : [expected],
|
||||
);
|
||||
expect(result.inlineScriptCount).toBe(expected === null ? 0 : 1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['href="external.js"', ["external.js"]],
|
||||
['xlink:href="external.js"', ["external.js"]],
|
||||
['href="first.js" xlink:href="second.js"', ["first.js"]],
|
||||
['xlink:href="second.js" href="first.js"', ["first.js"]],
|
||||
['href="" xlink:href="ignored.js"', []],
|
||||
['other:href="ignored.js"', []],
|
||||
['src="ignored.js"', []],
|
||||
])("extracts SVG script URL from %s", (attributes, paths) => {
|
||||
const result = htmlArtifactReferences(
|
||||
`<svg><script ${attributes}></script></svg>`,
|
||||
);
|
||||
expect(result.scripts.map(({ scriptPath }) => scriptPath)).toEqual(paths);
|
||||
expect(result.inlineScriptCount).toBe(0);
|
||||
});
|
||||
|
||||
it("shares precise body coordinates and location limits across HTML and SVG", () => {
|
||||
const prefix = '<!doctype html>\n<svg><script src="ignored.js">';
|
||||
const body = "\nrun();\n";
|
||||
expect(
|
||||
htmlArtifactReferences(`${prefix}${body}</script></svg>`).inlineScripts,
|
||||
).toEqual([
|
||||
{
|
||||
scriptType: "classic",
|
||||
location: { start: { line: 2, column: 30 }, end: { line: 4, column: 0 } },
|
||||
},
|
||||
]);
|
||||
const repeated = "<script>x</script><svg><script>y</script></svg>".repeat(60);
|
||||
const result = htmlArtifactReferences(repeated);
|
||||
expect(result.inlineScriptCount).toBe(120);
|
||||
expect(result.inlineScripts).toHaveLength(HTML_INLINE_SCRIPT_LOCATION_LIMIT);
|
||||
expect(
|
||||
htmlArtifactReferences("<template><svg><script>x</script></svg></template>")
|
||||
.inlineScriptCount,
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("retains exact inline script body coordinates, including unclosed scripts", () => {
|
||||
const prefix = "<!doctype html>\n<script>";
|
||||
const body = "\nfetch('/api');\n";
|
||||
|
||||
@@ -80,6 +80,30 @@ const javaScriptType = (
|
||||
const asciiLowercase = (value: string): string =>
|
||||
value.replace(/[A-Z]/gu, (letter) => letter.toLowerCase());
|
||||
|
||||
/** SVG uses href/XLink and has no HTML language fallback or src attribute. */
|
||||
const scriptAttributes = (node: DefaultTreeAdapterTypes.Element) => {
|
||||
const html = node.namespaceURI === "http://www.w3.org/1999/xhtml";
|
||||
const source = html
|
||||
? node.attrs.find(({ name }) => name === "src")
|
||||
: (node.attrs.find(
|
||||
({ name, namespace }) => name === "href" && namespace === undefined,
|
||||
) ??
|
||||
node.attrs.find(
|
||||
({ name, namespace }) =>
|
||||
name === "href" && namespace === "http://www.w3.org/1999/xlink",
|
||||
));
|
||||
return {
|
||||
source,
|
||||
scriptType: javaScriptType(
|
||||
html
|
||||
? node.attrs
|
||||
: node.attrs.filter(
|
||||
({ name, namespace }) => name === "type" && namespace === undefined,
|
||||
),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract inert HTML script references, inline JavaScript bodies, and the
|
||||
* first document base href.
|
||||
@@ -105,15 +129,19 @@ export const htmlArtifactReferences = (
|
||||
if (node === undefined) continue;
|
||||
if (
|
||||
defaultTreeAdapter.isElementNode(node) &&
|
||||
node.namespaceURI === "http://www.w3.org/1999/xhtml"
|
||||
(node.namespaceURI === "http://www.w3.org/1999/xhtml" ||
|
||||
node.namespaceURI === "http://www.w3.org/2000/svg")
|
||||
) {
|
||||
if (node.tagName === "base" && baseHref === null) {
|
||||
if (
|
||||
node.namespaceURI === "http://www.w3.org/1999/xhtml" &&
|
||||
node.tagName === "base" &&
|
||||
baseHref === null
|
||||
) {
|
||||
const href = node.attrs.find(({ name }) => name === "href");
|
||||
if (href !== undefined) baseHref = href.value;
|
||||
}
|
||||
if (node.tagName === "script") {
|
||||
const scriptType = javaScriptType(node.attrs);
|
||||
const src = node.attrs.find(({ name }) => name === "src");
|
||||
const { scriptType, source: src } = scriptAttributes(node);
|
||||
const location = node.sourceCodeLocation?.startTag;
|
||||
if (src !== undefined && src.value !== "" && location !== undefined) {
|
||||
scripts.push({
|
||||
|
||||
@@ -7,6 +7,83 @@ import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/j
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"preserves SVG script references and reports skipped executable bodies",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-svg-html-cli-");
|
||||
const inlineUrl = "https://example.test/svg-inline";
|
||||
const html = [
|
||||
"<!doctype html><svg>",
|
||||
`<script src="ignored.js" language="vbscript">fetch("${inlineUrl}");</script>`,
|
||||
'<script href="main.js">ignored();</script>',
|
||||
'<script xlink:href="main.js">ignored();</script>',
|
||||
'<script href="" xlink:href="main.js">ignored();</script>',
|
||||
'<script type="application/json">{"x":1}</script>',
|
||||
"</svg><template><svg><script>ignored();</script></svg></template>",
|
||||
].join("\n");
|
||||
await writeFile(join(root, "index.html"), html);
|
||||
await writeFile(
|
||||
join(root, "main.js"),
|
||||
'fetch("https://example.test/external");',
|
||||
);
|
||||
const response = await cli.run({
|
||||
arguments: [
|
||||
"analyze-javascript-application",
|
||||
root,
|
||||
"--source-path",
|
||||
"index.html",
|
||||
"--source-path",
|
||||
"main.js",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse(
|
||||
parseEvidence(response.json).normalized_result,
|
||||
);
|
||||
const observations = result.graph.nodes.flatMap(
|
||||
({ observations }) => observations,
|
||||
);
|
||||
const scopes = observations.filter(
|
||||
({ properties }) => properties.operation === "parse-html-inline-scripts",
|
||||
);
|
||||
expect(scopes).toHaveLength(1);
|
||||
expect(scopes[0]).toMatchObject({
|
||||
properties: {
|
||||
path: "index.html",
|
||||
inline_script_count: 1,
|
||||
inline_scripts: [
|
||||
{
|
||||
script_type: "classic",
|
||||
location: {
|
||||
start: { line: 2, column: 45 },
|
||||
end: { line: 2, column: 86 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
evidence: {
|
||||
state: "unavailable",
|
||||
confidence: "unknown",
|
||||
coverage: { status: "partial" },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
result.graph.edges.filter(
|
||||
({ relation, properties }) =>
|
||||
relation === "loads" && properties.resolved_path === "main.js",
|
||||
),
|
||||
).toHaveLength(2);
|
||||
const endpoints = result.graph.nodes
|
||||
.filter(({ kind }) => kind === "endpoint")
|
||||
.flatMap(({ observations }) => observations)
|
||||
.map(({ properties }) => properties.value);
|
||||
expect(endpoints).toContain("https://example.test/external");
|
||||
expect(endpoints).not.toContain(inlineUrl);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for([
|
||||
"invalid-html",
|
||||
"empty-html",
|
||||
|
||||
Reference in new issue
Block a user