Skip to content

Fix CSP nonce validation and violation reporting for external scripts#40956

Merged
TimvdLippe merged 3 commits intoservo:mainfrom
dyegoaurelio:nonce-external-scripts
Feb 27, 2026
Merged

Fix CSP nonce validation and violation reporting for external scripts#40956
TimvdLippe merged 3 commits intoservo:mainfrom
dyegoaurelio:nonce-external-scripts

Conversation

@dyegoaurelio
Copy link
Copy Markdown
Contributor

This PR fixes two related issues with Content Security Policy (CSP) nonce validation for external scripts:

  1. Missing nonce validation for external scripts with malformed attributes
  2. Incorrect violation event reporting for blocked external resources

This makes servo closer to passing the nonce-enforce-blocked wpt test.

The remaining failures are blocked by required changes in the html parser.

  1. Svg script support (Implement parsing of scripts in SVG html5ever#118)
<svg xmlns="http://www.w3.org/2000/svg">
<script attribute attribute nonce="abc">
    t.unreached_func("Duplicate attribute in SVG, no execution.")();
</script>
</svg>
  1. Duplicate attrs check
    the html parser needs to provide this flag, as mentioned on the original commit message (4821bc0)
<script attribute attribute nonce="abc">
    t.unreached_func("Duplicate attribute, no execution.")();
</script>
<script attribute attribute=<style nonce="abc">
    t.unreached_func("2# Duplicate attribute, no execution.")();
</script>

[...]

<script src="../support/nonce-should-be-blocked.js?5" attribute attribute nonce="abc"></script>

I've also created a PR to implement the duplicate attrs flag on html5ever servo/html5ever#695

Testing: doesn't fixes the aforementioned wpt test yet.
Fixes: part of #36437

@dyegoaurelio
Copy link
Copy Markdown
Contributor Author

dyegoaurelio commented Nov 29, 2025

marked as draft because I created a couple wpt failures https://github.com/dyegoaurelio/servo/actions/runs/19787946509

@dyegoaurelio dyegoaurelio force-pushed the nonce-external-scripts branch from 0fef75f to 074e678 Compare November 30, 2025 02:48
@dyegoaurelio dyegoaurelio marked this pull request as ready for review November 30, 2025 02:48
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Nov 30, 2025
@TimvdLippe TimvdLippe added the T-linux-wpt Do a try run of the WPT label Nov 30, 2025
@github-actions github-actions Bot removed the T-linux-wpt Do a try run of the WPT label Nov 30, 2025
@github-actions
Copy link
Copy Markdown

🔨 Triggering try run (#19796121125) for Linux (WPT)

@github-actions
Copy link
Copy Markdown

Test results for linux-wpt from try job (#19796121125):

Flaky unexpected result (29)
  • OK /FileAPI/url/url-with-fetch.any.html (#21517)
    • FAIL [expected PASS] subtest: Revoke blob URL after calling fetch, fetch should succeed

      promise_test: Unhandled rejection with value: object "TypeError: Network error occurred"
      

  • OK /IndexedDB/idbfactory_open.any.html
    • FAIL [expected PASS] subtest: Calling open() with version argument 1.5 should not throw.

      assert_equals: version expected 1 but got 9007199254740991
      

  • PASS [expected FAIL] /_mozilla/css/linear_gradients_reverse_a.html
  • OK /_mozilla/mozilla/getBoundingClientRect.html (#39668)
    • FAIL [expected PASS] subtest: getBoundingClientRect 1

      assert_equals: expected 62 but got 60.35
      

  • CRASH [expected OK] /_mozilla/mozilla/preserve_wrapper_callback.html
  • OK /_mozilla/webxr/create_session.https.html
    • FAIL [expected PASS] subtest: create_session

      can't access property "simulateDeviceConnection", navigator.xr.test is undefined
      

  • CRASH [expected OK] /_webgl/conformance/glsl/implicit/subtract_int_float.vert.html
  • CRASH [expected ERROR] /_webgl/conformance2/textures/image/tex-2d-rgb32f-rgb-float.html
  • TIMEOUT [expected CRASH] /content-security-policy/inheritance/blob-url-in-main-window-self-navigate-inherits.sub.html
  • TIMEOUT /content-security-policy/inheritance/location-reload.html (#38983)
    • FAIL [expected PASS] subtest: location.reload() of empty iframe.

      assert_equals: Image should be blocked by CSP after reload. expected "img blocked" but got "img loaded"
      

  • FAIL [expected PASS] /css/css-backgrounds/background-size-042.html
  • OK /css/css-fonts/generic-family-keywords-002.html (#40929)
    • PASS [expected FAIL] subtest: font-family: -webkit-serif treated as &lt;font-family&gt;, not &lt;generic-name&gt;
    • PASS [expected FAIL] subtest: font-family: -webkit-sans-serif treated as &lt;font-family&gt;, not &lt;generic-name&gt;
    • PASS [expected FAIL] subtest: font-family: -webkit-cursive treated as &lt;font-family&gt;, not &lt;generic-name&gt;
    • PASS [expected FAIL] subtest: font-family: -webkit-fantasy treated as &lt;font-family&gt;, not &lt;generic-name&gt;
    • PASS [expected FAIL] subtest: font-family: -webkit-monospace treated as &lt;font-family&gt;, not &lt;generic-name&gt;
    • PASS [expected FAIL] subtest: font-family: -webkit-system-ui treated as &lt;font-family&gt;, not &lt;generic-name&gt;
    • PASS [expected FAIL] subtest: font-family: -webkit-math treated as &lt;font-family&gt;, not &lt;generic-name&gt;
    • FAIL [expected PASS] subtest: font-family: -webkit-generic(fangsong) treated as &lt;font-family&gt;, not &lt;generic-name&gt;

      assert_equals: expected 50 but got 30
      

    • FAIL [expected PASS] subtest: font-family: -webkit-generic(kai) treated as &lt;font-family&gt;, not &lt;generic-name&gt;

      assert_equals: expected 50 but got 30
      

    • FAIL [expected PASS] subtest: font-family: -webkit-generic(khmer-mul) treated as &lt;font-family&gt;, not &lt;generic-name&gt;

      assert_equals: expected 50 but got 30
      

    • And 12 more unexpected results...
  • CRASH [expected OK] /html/browsers/browsing-the-web/history-traversal/001.html
  • OK /html/browsers/browsing-the-web/navigating-across-documents/005.html (#27062)
    • PASS [expected FAIL] subtest: Link with onclick navigation and href navigation
  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html (#28691)
    • FAIL [expected PASS] subtest: load event does not fire on window.open('about:blank')

      assert_unreached: load should not be fired Reached unreachable code
      

  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/update-the-rendering.html (#24145)
    • TIMEOUT [expected FAIL] subtest: "Flush autofocus candidates" should be happen before a scroll event and animation frame callbacks

      Test timed out
      

  • CRASH [expected OK] /html/semantics/document-metadata/the-link-element/stylesheet-non-OK-status.html
  • OK [expected TIMEOUT] /html/semantics/embedded-content/media-elements/src_object_blob.html (#40340)
    • PASS [expected TIMEOUT] subtest: HTMLMediaElement.srcObject blob
  • OK /html/semantics/forms/form-submission-0/multipart-formdata.window.html (#28725)
    • PASS [expected FAIL] subtest: multipart/form-data: 0x00 in value (normal form)
  • OK /html/semantics/forms/form-submission-0/urlencoded2.window.html (#28687)
    • PASS [expected FAIL] subtest: application/x-www-form-urlencoded: Basic File test (formdata event)
  • OK /html/semantics/scripting-1/the-script-element/module/dynamic-import/blob-url.any.html (#33948)
    • FAIL [expected PASS] subtest: Revoking a blob URL immediately after calling import will not fail

      promise_test: Unhandled rejection with value: object "TypeError: Dynamic import failed"
      

  • OK /preload/prefetch-document.html (#37210)
    • FAIL [expected PASS] subtest: different-site document prefetch with 'as=document' should not be consumed

      assert_equals: expected 2 but got 1
      

  • OK /resource-timing/buffer-full-add-then-clear.html (#40819)
    • PASS [expected FAIL] subtest: Test that if the buffer is cleared after entries were added to the secondary buffer, those entries make it into the primary one
  • CRASH [expected OK] /trusted-types/Element-setAttribute-setAttributeNS-sinks.tentative.html
  • TIMEOUT /trusted-types/trusted-types-navigation.html?06-10 (#37920)
    • PASS [expected FAIL] subtest: Navigate a frame via anchor with javascript:-urls in report-only mode.
  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?26-30 (#38807)
    • PASS [expected TIMEOUT] subtest: Navigate a window via form-submission with javascript:-urls in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ default policy in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls in enforcing mode.
    • PASS [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in enforcing mode.
  • CRASH [expected OK] /trusted-types/trusted-types-reporting-check-report-DedicatedWorker-sink-mismatch.html
  • CRASH [expected TIMEOUT] /wasm/webapi/empty-body.any.worker.html
  • OK /webdriver/tests/classic/perform_actions/wheel.py
    • FAIL [expected PASS] subtest: test_scroll_iframe

      webdriver.error.TimeoutException: timeout (500): Timed out after 0.5 seconds with message: Didn't receive all events: expected at least 1, got 0
      

Stable unexpected results that are known to be intermittent (21)
  • TIMEOUT /FileAPI/url/url-in-tags-revoke.window.html (#19978)
    • TIMEOUT [expected PASS] subtest: Fetching a blob URL immediately before revoking it works in &lt;script&gt; tags.

      Test timed out
      

  • OK /IndexedDB/idbobjectstore_getAll.any.html (#39276)
    • PASS [expected FAIL] subtest: Get all values with transaction.commit()
  • OK /IndexedDB/idbobjectstore_getAll.any.worker.html (#39400)
    • PASS [expected FAIL] subtest: Get all values with transaction.commit()
  • OK /_mozilla/css/offset_properties_inline.html (#40543)
    • FAIL [expected PASS] subtest: offsetTop

      assert_equals: offsetTop of #inline-1 should be 0. expected 0 but got -1
      

    • FAIL [expected PASS] subtest: offsetLeft

      assert_equals: offsetLeft of #inline-2 should be 40. expected 40 but got 25
      

  • FAIL [expected PASS] /_mozilla/mozilla/sslfail.html (#10760)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window_resize_event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • OK /content-security-policy/frame-ancestors/frame-ancestors-path-ignored.window.html (#36468)
    • FAIL [expected PASS] subtest: A 'frame-ancestors' CSP directive with a URL that includes a path should be ignored.

      assert_unreached: The IFrame should have been blocked (or cross-origin). It wasn't. Reached unreachable code
      

  • OK /css/css-cascade/layer-font-face-override.html (#35935)
    • FAIL [expected PASS] subtest: @font-face override update with appended sheet 1

      assert_equals: expected "80px" but got "41.45px"
      

    • FAIL [expected PASS] subtest: @font-face override update with appended sheet 2

      assert_equals: expected "80px" but got "41.45px"
      

  • OK /css/css-fonts/generic-family-keywords-001.html (#37467)
    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(fangsong)

      assert_equals: quoted generic(fangsong) matches  @font-face rule expected 50 but got 30
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(khmer-mul)

      assert_equals: quoted generic(khmer-mul) matches  @font-face rule expected 50 but got 30
      

  • OK /fetch/metadata/generated/css-font-face.https.sub.tentative.html (#32732)
    • PASS [expected FAIL] subtest: sec-fetch-mode
    • FAIL [expected PASS] subtest: sec-fetch-storage-access - Cross-site

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

  • OK /fetch/metadata/generated/css-font-face.sub.tentative.html (#34624)
    • FAIL [expected PASS] subtest: sec-fetch-storage-access - Not sent to non-trustworthy same-origin destination

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • FAIL [expected PASS] subtest: sec-fetch-storage-access - Not sent to non-trustworthy same-site destination

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

  • OK /fetch/metadata/generated/element-img-environment-change.https.sub.html (#30111)
    • FAIL [expected PASS] subtest: sec-fetch-site - Cross-site, no attributes

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • FAIL [expected PASS] subtest: sec-fetch-site - Same site, no attributes

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • PASS [expected FAIL] subtest: sec-fetch-site - Same-Origin -&gt; Cross-Site, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - Same-Site -&gt; Same Origin, no attributes

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • PASS [expected FAIL] subtest: sec-fetch-site - Same-Site -&gt; Same-Site, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-mode - attributes: crossorigin=anonymous

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • PASS [expected FAIL] subtest: sec-fetch-mode - attributes: crossorigin=use-credentials
    • PASS [expected FAIL] subtest: sec-fetch-user - no attributes
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Same site, no attributes
  • OK /fetch/metadata/generated/element-img-environment-change.sub.html (#30111)
    • PASS [expected FAIL] subtest: sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • PASS [expected FAIL] subtest: sec-fetch-mode - Not sent to non-trustworthy same-site destination, no attributes
    • PASS [expected FAIL] subtest: sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no attributes
    • PASS [expected FAIL] subtest: sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • PASS [expected NOTRUN] subtest: Non-HTMLElement should not support autofocus
    • TIMEOUT [expected FAIL] subtest: Host element with delegatesFocus should support autofocus

      Test timed out
      

    • NOTRUN [expected FAIL] subtest: Host element with delegatesFocus including no focusable descendants should be skipped
    • NOTRUN [expected FAIL] subtest: Area element should support autofocus
  • OK /html/semantics/embedded-content/media-elements/preserves-pitch.html (#40352)
    • FAIL [expected PASS] subtest: Speed-ups should not change the pitch when preservesPitch=true

      assert_approx_equals: The actual pitch should be close to the expected pitch. expected 440 +/- 66 but got 0
      

  • OK /html/semantics/embedded-content/the-iframe-element/iframe-loading-lazy-reload-location-reload.html (#32595)
    • FAIL [expected PASS] subtest: Reloading iframe loading='lazy' before it is loaded: location.reload

      uncaught exception: Error: assert_equals: expected "http://web-platform.test:8000/html/semantics/embedded-content/the-iframe-element/support/blank.htm?src" but got "about:blank"
      

  • TIMEOUT [expected ERROR] /html/semantics/links/links-created-by-a-and-area-elements/target_blank_implicit_noopener_base.html (#40347)
  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • PASS [expected FAIL] subtest: Reload domComplete &gt; Original domComplete
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventStart &gt; Original domContentLoadedEventStart
    • PASS [expected FAIL] subtest: Reload domInteractive &gt; Original domInteractive
    • PASS [expected FAIL] subtest: Reload fetchStart &gt; Original fetchStart
    • PASS [expected FAIL] subtest: Reload loadEventEnd &gt; Original loadEventEnd
    • PASS [expected FAIL] subtest: Reload loadEventStart &gt; Original loadEventStart
  • OK /preload/preload-error.sub.html (#37177)
    • FAIL [expected PASS] subtest: success (fetch): main

      assert_greater_than: http://web-platform.test:8000/preload/resources/dummy.xml?label=fetch should be loaded expected a number greater than 0 but got 0
      

    • FAIL [expected PASS] subtest: CORS (fetch): main

      assert_greater_than: http://not-web-platform.test:8000/preload/resources/dummy.xml?pipe=header%28Access-Control-Allow-Origin%2C*%29&amp;label=fetch should be loaded expected a number greater than 0 but got 0
      

  • TIMEOUT [expected OK] /webstorage/localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.html (#29053)
    • TIMEOUT [expected PASS] subtest: StorageKey: test 3P about:blank window opened from a 3P iframe

      Test timed out
      

  • OK [expected ERROR] /webxr/render_state_update.https.html (#27535)

@github-actions
Copy link
Copy Markdown

✨ Try run (#19796121125) succeeded.

Copy link
Copy Markdown
Contributor

@TimvdLippe TimvdLippe left a comment

Choose a reason for hiding this comment

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

Nice fixes! I do think we can improve the handling the source position since the is_inline boolean seems duplicate information to me.

Comment thread components/script/dom/security/csp.rs Outdated
};
// Determine source location based on violation type and whether position was provided
let (source_file, line_number, column_number) =
if source_position_was_provided || is_inline {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't these be both true? E.g. the source position should have only been provided if it was inline. Therefore, I think we should assert that if the resource is URL, no source position is provided. Then only in the URL case we set the filename and for the other ones we use the source position.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The URL assertion broke the report for images. I reverted the assertion and removed the source position parameter from all URL reports aside from the html image element one

@servo-highfive servo-highfive added S-needs-code-changes Changes have not yet been made that were requested by a reviewer. and removed S-awaiting-review There is new code that needs to be reviewed. labels Nov 30, 2025
@dyegoaurelio dyegoaurelio force-pushed the nonce-external-scripts branch from 074e678 to b28b229 Compare November 30, 2025 16:33
@servo-highfive servo-highfive added S-awaiting-review There is new code that needs to be reviewed. and removed S-needs-code-changes Changes have not yet been made that were requested by a reviewer. labels Nov 30, 2025
@dyegoaurelio dyegoaurelio force-pushed the nonce-external-scripts branch from b28b229 to d666287 Compare November 30, 2025 16:34
@dyegoaurelio dyegoaurelio force-pushed the nonce-external-scripts branch 2 times, most recently from 051ada5 to 4f41daa Compare December 1, 2025 17:07
@dyegoaurelio dyegoaurelio marked this pull request as draft December 1, 2025 17:39
@dyegoaurelio dyegoaurelio marked this pull request as ready for review December 1, 2025 18:45
Comment thread components/script/dom/security/csp.rs Outdated
source_position.column_number,
)
} else {
(self.get_url().to_string(), 0, 0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like we are over-complicating things here and we would be better off fixing the callers of this method. Moreover, since self.get_url() is the URL for the globalscope, not the resource. Therefore, I think in almost all cases this URL is wrong since we expect the URL of the blocked resource, not the global it is part of?

Instead, can we update all caller-sides to provide the relevant information. If I understand you correctly, in the following cases we should set the filename, but not the line numbers:

  • Link elements
  • Image elements

And for script elements, it depends whether it is inline or external.

If that's the case, can we revert the changes made in this function and update the elements to pass in the right source position based on these expectations?

Additionally, it might be good to split up the PR as it fixes two distinct issues. One is related to nonce values, the other one is related to the source position. Therefore, I suggest to split it up in those two, to ease reviewing.

Lastly, it would help if WPT tests start passing, yet none are with this PR thus far. I understand that's blocked on the other mentioned issue in the HTML parser, so that's a bit unfortunate. It could be good to write a WPT test that only tests for one thing (e.g. the URL without line and column number) and then we defer passing the other tests until the HTML parser is updated.

Does that help you further?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Thanks for the review!

It could be good to write a WPT test that only tests for one thing (e.g. the URL without line and column number) and then we defer passing the other tests until the HTML parser is updated.

I didn't realize I could just add a new wpt test. I thought they were strictly pulled from upstream.
I agree that it's much better this way. I'll add a new test

If I understand you correctly, in the following > cases we should set the filename, but not the line numbers:

Link elements
Image elements

TBH, I'm not sure about the correct behavior here.

All the changes I made on this regard were because of test assertions:

  1. I've removed the line number from the script sources because

    if (e.lineNumber) {
    // Verify that the line is expected, then clear the expectation:
    assert_true(expectations[e.lineNumber], "Line number: " + e.lineNumber);
    assert_equals(e.blockedURI, "inline");
    } else {
    // Otherwise, verify that the URL is expected, then clear the expectation:
    var url = new URL(e.blockedURI);
    assert_true(expectations[url.pathname + url.search], "URL: " + e.blockedURI);
    }
    expects line number not to be truthy in order for it to check the blockedURI

  2. after your feedback I reworked it to not allow url resources (eb1d86a). But then It broke

    var document_url = base_url + "reporting-api/reporting-api-sends-reports-on-violation.https.sub.html";
    assert_equals(reports[0].type, "csp-violation");
    assert_equals(reports[0].url, document_url);
    assert_equals(reports[0].body.documentURL, document_url);
    assert_equals(reports[0].body.referrer, "");
    assert_equals(reports[0].body.blockedURL,
    base_url + "support/fail.png");
    assert_equals(reports[0].body.effectiveDirective, "img-src");
    assert_equals(reports[0].body.originalPolicy,
    "script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-group");
    assert_equals(reports[0].body.sourceFile, document_url);
    assert_equals(reports[0].body.sample, "");
    assert_equals(reports[0].body.disposition, "enforce");
    assert_equals(reports[0].body.statusCode, 200);
    assert_equals(reports[0].body.lineNumber, 53);
    assert_equals(reports[0].body.columnNumber, 0);
    Because this one expects both to be defined. That's why I reverted the assertion and added the line numbers back for the img reports.

Also, I just noticed that the img test is checking the event body.blockedURL while the script one is checking e.blockedURI

Maybe they require slightly different handling. I'm not sure.

I'll try to read the spec more thoughtfully later to get a better understanding of the correct way to build these objects.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the investigation! Note that for the second test, only Chrome passes it: https://wpt.fyi/results/content-security-policy/reporting-api/reporting-api-sends-reports-on-violation.https.sub.html?label=experimental&label=master&aligned

If we can't figure out a reasonable behaviour that passes all tests, we can also file an issue with the spec. For example, I am confused why that line expectation is 53, yet that's the line that has the script tag. Safari doesn't report a line number at all. Shouldn't it be line 49?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As Per the spec:

If the user agent is currently executing script, and can extract a source file's URL, line number, and column number from the global, set violation's source file, line number, and column number accordingly.

For declarative HTML loads (external scripts, links, preloads), no script is executing, so source position should be empty per spec. I removed the explicit source_position from those fetch handlers

However, the spec's definition of "source file" is acknowledged as underspecified, and Chrome populates source position for element-initiated image loads. So I kept the image element's source_position to avoid regressing report-to-directive-allowed-in-meta.https.sub.html.

We're also passing the trusted-types/navigate-to-javascript-url-001.html test now due to the column number fix

@dyegoaurelio dyegoaurelio marked this pull request as draft December 2, 2025 22:18
@dyegoaurelio dyegoaurelio force-pushed the nonce-external-scripts branch from e80d72a to 2e7c6be Compare February 27, 2026 00:40
@dyegoaurelio dyegoaurelio marked this pull request as ready for review February 27, 2026 00:41
@TimvdLippe TimvdLippe added the T-linux-wpt Do a try run of the WPT label Feb 27, 2026
@github-actions github-actions Bot removed the T-linux-wpt Do a try run of the WPT label Feb 27, 2026
@github-actions
Copy link
Copy Markdown

🔨 Triggering try run (#22476966400) for Linux (WPT)

@github-actions
Copy link
Copy Markdown

Test results for linux-wpt from try job (#22476966400):

Flaky unexpected result (32)
  • OK [expected TIMEOUT] /IndexedDB/idbfactory_open.any.worker.html
    • PASS [expected FAIL] subtest: Calling open() with version argument 1.5 should not throw.
    • PASS [expected TIMEOUT] subtest: Calling open() with version argument 9007199254740991 should not throw.
    • PASS [expected TIMEOUT] subtest: Calling open() with version argument undefined should not throw.
  • OK /_mozilla/css/offset_properties_inline.html (#40543)
    • FAIL [expected PASS] subtest: offsetTop

      assert_equals: offsetTop of #inline-1 should be 0. expected 0 but got -1
      

    • FAIL [expected PASS] subtest: offsetLeft

      assert_equals: offsetLeft of #inline-2 should be 40. expected 40 but got 25
      

  • OK /_mozilla/mozilla/getBoundingClientRect.html (#39668)
    • FAIL [expected PASS] subtest: getBoundingClientRect 1

      assert_equals: expected 62 but got 60.35
      

  • CRASH [expected PASS] /_mozilla/shadow-dom/move-element-with-ua-shadow-tree-crash.html (#39473)
  • ERROR [expected OK] /_webgl/conformance/glsl/functions/glsl-function.html
  • OK /content-security-policy/frame-ancestors/frame-ancestors-path-ignored.window.html (#36468)
    • PASS [expected FAIL] subtest: A 'frame-ancestors' CSP directive with a URL that includes a path should be ignored.
  • OK /css/css-cascade/layer-font-face-override.html (#35935)
    • FAIL [expected PASS] subtest: @font-face override update with appended sheet 1

      assert_equals: expected "80px" but got "38.3166666666667px"
      

    • FAIL [expected PASS] subtest: @font-face override update with appended sheet 2

      assert_equals: expected "80px" but got "38.3166666666667px"
      

  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted monospace (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted system-ui (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted math (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong) (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(khmer-mul) (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(nastaliq) (drawing text in a canvas)
  • OK [expected ERROR] /fetch/fetch-later/quota/same-origin-iframe/multiple-iframes.https.window.html (#35176)
  • OK /fetch/metadata/generated/css-font-face.https.sub.tentative.html (#32732)
    • PASS [expected FAIL] subtest: sec-fetch-mode
    • PASS [expected FAIL] subtest: sec-fetch-user
  • OK [expected ERROR] /fetch/metadata/window-open.https.sub.html (#40339)
  • ERROR [expected OK] /focus/focus-event-after-switching-iframes.sub.html (#40368)
  • CRASH [expected TIMEOUT] /html/browsers/browsing-the-web/navigating-across-documents/cross-origin-top-navigation-without-user-activation.window.html
  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html (#28691)
    • FAIL [expected PASS] subtest: load event does not fire on window.open('about:blank')

      assert_unreached: load should not be fired Reached unreachable code
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.html (#29049)
    • PASS [expected FAIL] subtest: Same-origin navigation started from unload handler must be ignored
  • OK /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html (#28697)
    • FAIL [expected PASS] subtest: aElement.click() before the load event must NOT replace

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?thereplacement" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/code-injector.html?pipe=sub(none)&amp;code=%0A%20%20%20%20const%20a%20%3D%20document.createElement(%22a%22)%3B%0A%20%20%20%20a.href%20%3D%20%22%2Fcommon%2Fblank.html%3Fthereplacement%22%3B%0A%20%20%20%20document.currentScript.before(a)%3B%0A%20%20%20%20a.click()%3B%0A%20%20"
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_2.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • TIMEOUT /html/semantics/embedded-content/media-elements/autoplay-disabled-by-feature-policy.https.sub.html (#41221)
    • FAIL [expected TIMEOUT] subtest: Feature-Policy header: autoplay "none" disallows same-origin iframes.

      assert_false: autoplay expected false got true
      

  • TIMEOUT /html/semantics/embedded-content/media-elements/preserves-pitch.html (#40352)
    • PASS [expected TIMEOUT] subtest: Speed-ups should not change the pitch when preservesPitch=true
    • PASS [expected NOTRUN] subtest: Slow-downs should not change the pitch when preservesPitch=true
    • TIMEOUT [expected NOTRUN] subtest: Speed-ups should change the pitch when preservesPitch=false

      Test timed out
      

  • TIMEOUT [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigate_other_frame_popup.sub.html (#39702)
    • TIMEOUT [expected FAIL] subtest: Sandboxed iframe can not navigate other frame's popup

      Test timed out
      

  • OK /html/semantics/forms/form-submission-0/jsurl-form-submit.tentative.html (#36489)
    • PASS [expected FAIL] subtest: Verifies that form submissions scheduled inside javascript: urls take precedence over the javascript: url's return value.
  • OK /html/semantics/scripting-1/the-script-element/execution-timing/077.html (#22139)
    • PASS [expected FAIL] subtest: adding several types of scripts through the DOM and removing some of them confuses scheduler
  • OK /html/semantics/scripting-1/the-script-element/module/dynamic-import/blob-url.any.html (#33948)
    • FAIL [expected PASS] subtest: Revoking a blob URL immediately after calling import will not fail

      promise_test: Unhandled rejection with value: object "TypeError: Module fetching failed"
      

  • TIMEOUT [expected OK] /html/user-activation/navigation-state-reset-sameorigin.html
    • TIMEOUT [expected PASS] subtest: Post-navigation state reset.

      Test timed out
      

  • CRASH [expected OK] /html/webappapis/scripting/processing-model-2/window-onerror-with-cross-frame-event-listeners-5.html
  • OK /html/webappapis/user-prompts/print-during-unload.html (#35944)
    • FAIL [expected PASS] subtest: print() during unload

      assert_array_equals: expected property 1 to be "destination" but got "error: window.print is not a function" (expected array ["start", "destination"] got ["start", "error: window.print is not a function"])
      

  • OK /pointerevents/compat/pointerevent_touch_target_after_pointerdown_target_removed.tentative.html (#42813)
    • PASS [expected FAIL] subtest: After a pointerdown listener moves the target to different position, touch events should be fired on the pointerdown target, but pointer events should be fired on the pointerdown target parent
    • PASS [expected FAIL] subtest: After a pointerdown listener moves the target to different position, touchmove event should be fired on the pointerdown target parent
  • OK /preload/link-header-preload-delay-onload.html (#39622)
    • FAIL [expected PASS] subtest: Makes sure that Link headers preload resources and block window.onload after resource discovery

      assert_true: expected true got false
      

  • OK /resource-timing/buffer-full-add-then-clear.html (#40819)
    • FAIL [expected PASS] subtest: Test that if the buffer is cleared after entries were added to the secondary buffer, those entries make it into the primary one

      assert_equals: Number of entries does not match the expected value. expected 3 but got 0
      

  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?06-10 (#37920)
    • PASS [expected FAIL] subtest: Navigate a frame via anchor with javascript:-urls in report-only mode.
    • PASS [expected TIMEOUT] subtest: Navigate a frame via anchor with javascript:-urls w/ default policy in report-only mode.
    • FAIL [expected NOTRUN] subtest: Navigate a window via anchor with javascript:-urls w/ a default policy throwing an exception in enforcing mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

    • FAIL [expected NOTRUN] subtest: Navigate a window via anchor with javascript:-urls w/ a default policy throwing an exception in report-only mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?26-30 (#38807)
    • PASS [expected TIMEOUT] subtest: Navigate a window via form-submission with javascript:-urls in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ default policy in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls in enforcing mode.
    • PASS [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in enforcing mode.
  • OK [expected ERROR] /webxr/render_state_update.https.html (#27535)
Stable unexpected results that are known to be intermittent (17)
  • TIMEOUT /FileAPI/url/url-in-tags-revoke.window.html (#19978)
    • TIMEOUT [expected PASS] subtest: Fetching a blob URL immediately before revoking it works in &lt;script&gt; tags.

      Test timed out
      

  • OK /IndexedDB/transaction-scheduling-mixed-scopes.any.html (#42753)
    • PASS [expected FAIL] subtest: Check that scope restrictions on mixed transactions are enforced.
  • FAIL [expected PASS] /_mozilla/mozilla/sslfail.html (#10760)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window_resize_event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • ERROR [expected CRASH] /_webgl/conformance2/misc/uninitialized-test-2.html (#41656)
  • OK /beacon/beacon-basic.https.window.html (#41723)
    • PASS [expected FAIL] subtest: Payload size restriction should be accumulated: type = string
  • OK /css/css-fonts/generic-family-keywords-001.html (#37467)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong)
    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(kai)

      assert_equals: quoted generic(kai) matches  @font-face rule expected 30 but got 50
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(khmer-mul)

      assert_equals: quoted generic(khmer-mul) matches  @font-face rule expected 30 but got 50
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(nastaliq)

      assert_equals: quoted generic(nastaliq) matches  @font-face rule expected 30 but got 50
      

  • OK /fetch/metadata/generated/css-font-face.sub.tentative.html (#34624)
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Not sent to non-trustworthy same-site destination
  • OK /html/browsers/history/the-history-interface/traverse_the_history_3.html (#21383)
    • FAIL [expected PASS] subtest: Multiple history traversals, last would be aborted

      assert_array_equals: Pages opened during history navigation expected property 1 to be 3 but got 2 (expected array [6, 3] got [6, 2])
      

  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html (#29087)
    • FAIL [expected TIMEOUT] subtest: &lt;dialog&gt;-contained autofocus element gets focused when the dialog is shown

      promise_test: Unhandled rejection with value: object "TypeError: can't access property "show", w.document.querySelector(...) is null"
      

  • TIMEOUT /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • FAIL [expected NOTRUN] subtest: Host element with delegatesFocus should support autofocus

      assert_equals: expected Element node &lt;div autofocus=""&gt;&lt;/div&gt; but got Element node &lt;body&gt;&lt;div autofocus=""&gt;&lt;/div&gt;&lt;/body&gt;
      

    • TIMEOUT [expected NOTRUN] subtest: Host element with delegatesFocus including no focusable descendants should be skipped

      Test timed out
      

  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/update-the-rendering.html (#24145)
    • TIMEOUT [expected FAIL] subtest: "Flush autofocus candidates" should be happen before a scroll event and animation frame callbacks

      Test timed out
      

  • OK /html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/allow-scripts-flag-changing-1.html (#39694)
    • PASS [expected FAIL] subtest: Meta refresh is blocked by the allow-scripts sandbox flag at its creation time, not when refresh comes due
  • OK /mixed-content/tentative/autoupgrades/video-upgrade.https.sub.html (#41135)
    • FAIL [expected PASS] subtest: Video autoupgraded

      assert_equals: Length. expected 1 but got Infinity
      

  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • PASS [expected FAIL] subtest: Reload domComplete &gt; Original domComplete
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventStart &gt; Original domContentLoadedEventStart
    • PASS [expected FAIL] subtest: Reload domInteractive &gt; Original domInteractive
    • PASS [expected FAIL] subtest: Reload fetchStart &gt; Original fetchStart
    • PASS [expected FAIL] subtest: Reload loadEventEnd &gt; Original loadEventEnd
    • PASS [expected FAIL] subtest: Reload loadEventStart &gt; Original loadEventStart
  • OK /resource-timing/test_resource_timing.https.html (#25216)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (iframe)
  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?31-35 (#38034)
    • PASS [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in report-only mode.
    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy throwing an exception in enforcing mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy throwing an exception in report-only mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy making the URL invalid in enforcing mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

@github-actions
Copy link
Copy Markdown

✨ Try run (#22476966400) succeeded.

Copy link
Copy Markdown
Contributor

@TimvdLippe TimvdLippe left a comment

Choose a reason for hiding this comment

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

Nice work, thanks for cleaning this up!

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Feb 27, 2026
@TimvdLippe TimvdLippe added this pull request to the merge queue Feb 27, 2026
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Feb 27, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Feb 27, 2026
@servo-highfive servo-highfive added S-tests-failed The changes caused existing tests to fail. and removed S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. labels Feb 27, 2026
External `<script>` elements with malformed attributes (e.g.,
`<script src="evil.js" <script nonce="abc">`) bypass the "is element
nonceable" check. Step 24 of "prepare the script element" was reading
`[[CryptographicNonce]]` directly via `nonce_value()`, skipping the
malformed-attribute check.

Add a nonceability guard at step 24: if the element has a nonce content
attribute but is not nonceable (malformed attributes per CSP §3.5),
strip the nonce. Elements without a nonce content attribute (e.g.
JS-created via `.nonce = "abc"`) use the internal slot directly, since
the nonceable check only applies to parser-created elements.

Spec: https://www.w3.org/TR/CSP/#is-element-nonceable

Signed-off-by: Dyego Aurélio <dyegoaurelio@gmail.com>
…etches

Fix compute_scripted_caller_source_position to return column 0 (not 1)
when no script is on the stack, by matching on Ok/Err from
describe_scripted_caller instead of using unwrap_or_default which
unconditionally applied the +1 offset.

Remove explicit source_position from script, link, and preload fetch
response CSP handlers. Image element keeps its source_position to
match chrome behavior asserted by report-to-directive-allowed-in-meta.https.sub.html.

Also fix the Convert<CSPViolationReportBody> impl to conditionalize
sourceFile/lineNumber/columnNumber on source_file being non-empty,
matching the existing report-uri serialization.

Spec: https://www.w3.org/TR/CSP/#create-violation-for-global

Signed-off-by: Dyego Aurélio <dyegoaurelio@gmail.com>
Add a custom servo WPT test that verifies nonce enforcement for both
inline and external scripts with malformed attributes. The test covers
the subset of nonce-enforce-blocked.html cases that Servo can currently
enforce without html5ever changes (duplicate-attribute detection, SVG
script support).

Remove expected failure for trusted-types/navigate-to-javascript-url-001
which now passes due to the column number fix in
compute_scripted_caller_source_position.

Signed-off-by: Dyego Aurélio <dyegoaurelio@gmail.com>
@dyegoaurelio dyegoaurelio force-pushed the nonce-external-scripts branch from 2e7c6be to 9bdc621 Compare February 27, 2026 12:31
@servo-highfive servo-highfive added S-awaiting-review There is new code that needs to be reviewed. and removed S-tests-failed The changes caused existing tests to fail. labels Feb 27, 2026
@TimvdLippe TimvdLippe added this pull request to the merge queue Feb 27, 2026
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Feb 27, 2026
Merged via the queue into servo:main with commit 4531667 Feb 27, 2026
33 checks passed
@servo-highfive servo-highfive removed the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Feb 27, 2026
@dyegoaurelio dyegoaurelio deleted the nonce-external-scripts branch March 2, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-awaiting-review There is new code that needs to be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants