Skip to content

fix(@angular/build): encode script type in auto-CSP loader - #34035

Open
yoggydev wants to merge 1 commit into
angular:mainfrom
yoggydev:fix-auto-csp-type-encoding
Open

fix(@angular/build): encode script type in auto-CSP loader#34035
yoggydev wants to merge 1 commit into
angular:mainfrom
yoggydev:fix-auto-csp-type-encoding

Conversation

@yoggydev

@yoggydev yoggydev commented Sep 8, 2026

Copy link
Copy Markdown

PR Checklist

Please check to confirm your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

createLoaderScript() in the auto-CSP generator interpolates four <script> attributes
into the generated loader source. Three are encoded; type is not:

const srcAttr         = encodeURI(s.src).replaceAll("'", "\\'");
const typeAttr        = s.type ? "'" + s.type + "'" : "''";
const integrityAttr   = JSON.stringify(s.integrity ?? null).replaceAll('<', '\\u003c');
const crossOriginAttr = JSON.stringify(s.crossOrigin ?? null).replaceAll('<', '\\u003c');

The comment above the function gives the reason it is safe to interpolate type directly:

// Can only be 'module' or a JS MIME type or an empty string.

That does not match the check the value actually passes. The gate is isJavascriptMimeType():

function isJavascriptMimeType(mimeType: string): boolean {
  return mimeType.split(';')[0] === 'text/javascript';
}

Only the part before the first ; is compared — correctly, since that mirrors how a browser
matches a script's MIME essence — so a value such as type="text/javascript;<parameters>"
passes the gate and reaches typeAttr unchanged. type is validated for the purpose of
deciding whether a script should be dynamically loaded, and then embedded into a JavaScript
string literal as though that validation had also constrained its characters.

Two consequences follow from index.html content alone:

  • a ' in the value closes the string literal, so the rest of the attribute becomes
    statements in the loader source. The generated loader is hashed into the emitted CSP, so
    such statements become part of the policy's trusted script hash.
  • a </script> in the value terminates the generated element, because the loader is written
    with rewriter.emitRaw(...). The .replaceAll('<', '\\u003c') on the two neighbouring
    attributes exists to prevent exactly this.

What is the new behavior?

type is encoded the same way as integrity and crossOrigin:

const typeAttr = JSON.stringify(s.type ?? '').replaceAll('<', '\\u003c');

Both branches of createLoaderScript() (Trusted Types enabled and disabled) share the same
srcListFormatted, so this single change covers both.

Two specs are added, one per case above. Existing specs that spell out the loader tuple are
updated for the quote style JSON.stringify produces ('' becomes "").

Does this PR introduce a breaking change?

  • Yes
  • No

The only observable difference is the quote style of the type slot in the generated loader,
which is internal to the emitted script.

Other information

This is a defense-in-depth change. I was not able to identify an attacker-controlled path to
the type attribute that does not already require control over the application's build input:
autoCsp() is reached only from tools/esbuild/index-html-generator.ts, once per build, over
index.html, and the <script> tags the CLI generates itself carry a literal type="module"
(augment-index-html.ts). Filing it as an ordinary fix for that reason.

createLoaderScript() interpolates four script attributes into the generated loader.
integrity and crossOrigin are encoded with JSON.stringify and \u003c, but type is
inserted directly into a single-quoted JavaScript string literal.

The comment above the function states that type can only be 'module', a JS MIME type
or an empty string, but isJavascriptMimeType() only compares the part before the
first ';', so a value such as text/javascript;<parameters> reaches the loader
unchanged. A quote in that value closes the string literal, and a closing script tag
terminates the generated element.

Encode type the same way as its neighbours. Both branches of createLoaderScript()
share srcListFormatted, so one change covers Trusted Types enabled and disabled.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the auto-csp utility to safely encode the script type attribute using JSON.stringify and escaping < characters. This prevents potential script injection or context escaping when a script type contains MIME parameters or HTML tags. The corresponding unit tests have been updated to reflect the double-quoted output, and new tests have been added to verify the encoding of MIME parameters and closing script tags. There are no review comments, so no further feedback is provided.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant