Skip to content

Conversation

@elomscansio
Copy link
Contributor

@elomscansio elomscansio commented May 4, 2025

The Document::set_csp_list method now appends new policies to any
existing CspList rather than replacing it. This ensures that CSPs
from multiple sources (e.g., HTTP headers and <meta> tags) are
merged as expected, following the spec.

This change avoids silently discarding header-defined policies when
a <meta http-equiv="Content-Security-Policy"> is present in the
document.


Copy link
Member

@jdm jdm left a comment

Choose a reason for hiding this comment

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

Thank you!

@jdm jdm added this pull request to the merge queue May 4, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks May 4, 2025
@jdm
Copy link
Member

jdm commented May 4, 2025

@TimvdLippe There are some trusted types failures with this change that might interest you!

@TimvdLippe
Copy link
Contributor

Are we sure this is the right behaviour? I thought that CSP lists would override and only inherit for local iframes.

@jdm
Copy link
Member

jdm commented May 4, 2025

Hmm. The intent here is that CSP lists from meta elements are appended to any CSP list received in the headers for a document. We may need to be more selective about this behavior change.

@jdm
Copy link
Member

jdm commented May 4, 2025

I was reading https://www.w3.org/TR/CSP/#multiple-policies as supporting this use case, and the note under https://www.w3.org/TR/CSP/#meta-element seems to reinforce this.

@TimvdLippe
Copy link
Contributor

I see no new WPT pass with this change. Does that mean the behaviour is untested (unlikely, but possible) or we already pass it or the patch is insufficient? I would expect at least one test to pass with this fix and then we can look into why the other ones started failing.

@jdm
Copy link
Member

jdm commented May 4, 2025

Yeah, it's worth writing a new test that checks this case explicitly.

@elomscansio
Copy link
Contributor Author

i'm tryna add test but no assert is run

@servo-wpt-sync
Copy link
Collaborator

🤖 Opened new upstream WPT pull request (web-platform-tests/wpt#52322) with upstreamable changes.

@jdm
Copy link
Member

jdm commented May 5, 2025

i'm tryna add test but no assert is run

The test throws an exception because the first policy is blocked by CSP—using the same directive name (trusted-types) for both CSP lists won't work, because each check needs to pass both rules. Instead we need to have rules for two different things (eg. img-src and media-src) and verify that both are blocked as expected.

@elomscansio
Copy link
Contributor Author

i'm tryna add test but no assert is run

The test throws an exception because the first policy is blocked by CSP—using the same directive name (trusted-types) for both CSP lists won't work, because each check needs to pass both rules. Instead we need to have rules for two different things (eg. img-src and media-src) and verify that both are blocked as expected.

just pushed, I used script-src instead of media-src, I could not find video file to load

@servo-wpt-sync
Copy link
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#52322).

Comment on lines +4206 to +4303
if let Some(new_list) = csp_list {
let mut current = self.get_csp_list();
match current {
Some(ref mut existing) => existing.append(new_list),
None => current = Some(new_list),
}
self.policy_container.borrow_mut().set_csp_list(current);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the test failures, I think this patch is incorrect. The test in question has the following:

<meta http-equiv="Content-Security-Policy" content="trusted-types 'none'">
<meta http-equiv="Content-Security-Policy" content="connect-src 'none'">

It has no CSP headers, so the only thing here are the meta elements.

Based on this patch, I see that it does two things when the second meta element is parsed:

  1. It adds the new list to current (by mutating the object)
  2. It sets the new list.

Notably, it doesn't clone the list, like the other implementation does. I don't know enough Rust, but I have a feeling that means it gets duplicated somehow.

Lastly, shouldn't we update the policy_container.set_csp_list instead of only the set_csp_list on document? We also have it on Window, so why are we only updating a single version of it?

All in all, it is unlikely to me that the Trusted Types code is wrong. Instead, I think the logic here somehow ends up duplicating the list the contents of the list twice.

Suggested change
if let Some(new_list) = csp_list {
let mut current = self.get_csp_list();
match current {
Some(ref mut existing) => existing.append(new_list),
None => current = Some(new_list),
}
self.policy_container.borrow_mut().set_csp_list(current);
}
if let Some(new_list) = csp_list {
let new_list = match self.get_csp_list() {
Some(original_list) => {
let mut combined_list = original_list.clone();
combined_list.append(new_list);
combined_list
},
None => new_list,
}
self.policy_container.borrow_mut().set_csp_list(Some(new_list));
}

Can you try with this patch and see if the Trusted Types tests still fail?

Copy link
Member

Choose a reason for hiding this comment

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

Lastly, shouldn't we update the policy_container.set_csp_list instead of only the set_csp_list on document? We also have it on Window, so why are we only updating a single version of it?

The only copy of the CSP list is in the policy container that lives in the document:

pub fn get_csp_list(&self) -> Option<CspList> {
if let Some(window) = self.downcast::<Window>() {
return window.Document().get_csp_list().map(|c| c.clone());
}

Copy link
Member

Choose a reason for hiding this comment

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

Notably, it doesn't clone the list, like the other implementation does. I don't know enough Rust, but I have a feeling that means it gets duplicated somehow.

No cloning is required. Document::get_csp_list already returns a clone, so the clone is updated and the original value is overwritten by the updated value.

Copy link
Contributor Author

@elomscansio elomscansio May 6, 2025

Choose a reason for hiding this comment

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

i added another same test to confirm the multiple meta header and its passing which did not help,
add added logging to the failing test (tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html, https://github.com/elomscansio/servo/blob/9fe700f23d9d24e307d1d3925a20ddcbb1da25c8/tests/wpt/tests/trusted-types/support/csp-violations.js#L22-L23) js and it turned out that the trusted-type in being thrown twice which i don't know why

0:07.42 pid:17791  0 .........................
 0:07.42 pid:17791 isTrusted true
 0:07.42 pid:17791 documentURI http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:07.42 pid:17791 referrer http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:07.42 pid:17791 blockedURI trusted-types-policy
 0:07.42 pid:17791 effectiveDirective trusted-types
 0:07.42 pid:17791 violatedDirective trusted-types
 0:07.42 pid:17791 originalPolicy trusted-types 'none'
 0:07.42 pid:17791 sourceFile http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:07.42 pid:17791 sample SomeName
 0:07.42 pid:17791 disposition enforce
 0:07.42 pid:17791 statusCode 200
 0:07.42 pid:17791 lineNumber 14
 0:07.42 pid:17791 columnNumber 27
 0:07.42 pid:17791 composedPath function composedPath() {
 0:07.42 pid:17791     [native code]
 0:07.42 pid:17791 }
 0:07.42 pid:17791 stopPropagation function stopPropagation() {
 0:07.42 pid:17791     [native code]
 0:07.42 pid:17791 }
 0:07.42 pid:17791 stopImmediatePropagation function stopImmediatePropagation() {
 0:07.42 pid:17791     [native code]
 0:07.42 pid:17791 }
 0:07.42 pid:17791 preventDefault function preventDefault() {
 0:07.42 pid:17791     [native code]
 0:07.42 pid:17791 }
 0:07.42 pid:17791 initEvent function initEvent() {
 0:07.42 pid:17791     [native code]
 0:07.42 pid:17791 }
 0:07.42 pid:17791 type securitypolicyviolation
 0:07.43 pid:17791 target …
 0:07.43 pid:17791 srcElement …
 0:07.44 pid:17791 currentTarget …
 0:07.44 pid:17791 eventPhase 2
 0:07.44 pid:17791 cancelBubble false
 0:07.44 pid:17791 bubbles true
 0:07.44 pid:17791 cancelable true
 0:07.44 pid:17791 returnValue true
 0:07.44 pid:17791 defaultPrevented false
 0:07.44 pid:17791 composed false
 0:07.44 pid:17791 timeStamp 2530.83
 0:07.44 pid:17791 NONE 0
 0:07.44 pid:17791 CAPTURING_PHASE 1
 0:07.44 pid:17791 AT_TARGET 2
 0:07.44 pid:17791 BUBBLING_PHASE 3
 0:07.44 pid:17791 
 0:07.44 pid:17791  1 .........................
 0:07.44 pid:17791 isTrusted true
 0:07.44 pid:17791 documentURI http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:07.44 pid:17791 referrer http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:07.44 pid:17791 blockedURI trusted-types-policy
 0:07.44 pid:17791 effectiveDirective trusted-types
 0:07.44 pid:17791 violatedDirective trusted-types
 0:07.44 pid:17791 originalPolicy trusted-types 'none'
 0:07.44 pid:17791 sourceFile http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:07.44 pid:17791 sample SomeName
 0:07.44 pid:17791 disposition enforce
 0:07.44 pid:17791 statusCode 200
 0:07.44 pid:17791 lineNumber 14
 0:07.44 pid:17791 columnNumber 27
 0:07.45 pid:17791 composedPath function composedPath() {
 0:07.45 pid:17791     [native code]
 0:07.45 pid:17791 }
 0:07.45 pid:17791 stopPropagation function stopPropagation() {
 0:07.45 pid:17791     [native code]
 0:07.45 pid:17791 }
 0:07.45 pid:17791 stopImmediatePropagation function stopImmediatePropagation() {
 0:07.45 pid:17791     [native code]
 0:07.45 pid:17791 }
 0:07.45 pid:17791 preventDefault function preventDefault() {
 0:07.45 pid:17791     [native code]
 0:07.45 pid:17791 }
 0:07.45 pid:17791 initEvent function initEvent() {
 0:07.45 pid:17791     [native code]
 0:07.45 pid:17791 }
 0:07.45 pid:17791 type securitypolicyviolation
 0:07.45 pid:17791 target …
 0:07.45 pid:17791 srcElement …
 0:07.45 pid:17791 currentTarget …
 0:07.45 pid:17791 eventPhase 2
 0:07.45 pid:17791 cancelBubble false
 0:07.45 pid:17791 bubbles true
 0:07.45 pid:17791 cancelable true
 0:07.45 pid:17791 returnValue true
 0:07.45 pid:17791 defaultPrevented false
 0:07.45 pid:17791 composed false
 0:07.45 pid:17791 timeStamp 2552.05
 0:07.45 pid:17791 NONE 0
 0:07.45 pid:17791 CAPTURING_PHASE 1
 0:07.45 pid:17791 AT_TARGET 2
 0:07.45 pid:17791 BUBBLING_PHASE 3
 0:07.45 pid:17791 
 0:07.46 pid:17791  2 .........................
 0:07.46 pid:17791 isTrusted true
 0:07.46 pid:17791 documentURI http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:07.46 pid:17791 referrer http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:07.46 pid:17791 blockedURI ws://common/blank.html
 0:07.46 pid:17791 effectiveDirective connect-src
 0:07.46 pid:17791 violatedDirective connect-src
 0:07.46 pid:17791 originalPolicy connect-src 'none'
 0:07.46 pid:17791 sourceFile
 0:07.46 pid:17791 sample
 0:07.46 pid:17791 disposition enforce
 0:07.46 pid:17791 statusCode 200
 0:07.46 pid:17791 lineNumber 0
 0:07.46 pid:17791 columnNumber 1
 0:07.46 pid:17791 composedPath function composedPath() {
 0:07.46 pid:17791     [native code]
 0:07.46 pid:17791 }
 0:07.46 pid:17791 stopPropagation function stopPropagation() {
 0:07.46 pid:17791     [native code]
 0:07.46 pid:17791 }
 0:07.46 pid:17791 stopImmediatePropagation function stopImmediatePropagation() {
 0:07.46 pid:17791     [native code]
 0:07.46 pid:17791 }
 0:07.46 pid:17791 preventDefault function preventDefault() {
 0:07.46 pid:17791     [native code]
 0:07.46 pid:17791 }
 0:07.46 pid:17791 initEvent function initEvent() {
 0:07.46 pid:17791     [native code]
 0:07.46 pid:17791 }
 0:07.46 pid:17791 type securitypolicyviolation
 0:07.46 pid:17791 target …
 0:07.46 pid:17791 srcElement …
 0:07.47 pid:17791 currentTarget …
 0:07.47 pid:17791 eventPhase 2
 0:07.47 pid:17791 cancelBubble false
 0:07.47 pid:17791 bubbles true
 0:07.47 pid:17791 cancelable true
 0:07.47 pid:17791 returnValue true
 0:07.47 pid:17791 defaultPrevented false
 0:07.47 pid:17791 composed false
 0:07.47 pid:17791 timeStamp 2566.36
 0:07.47 pid:17791 NONE 0
 0:07.47 pid:17791 CAPTURING_PHASE 1
 0:07.47 pid:17791 AT_TARGET 2
 0:07.47 pid:17791 BUBBLING_PHASE 3

Copy link
Contributor Author

@elomscansio elomscansio May 6, 2025

Choose a reason for hiding this comment

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

maybe there are some other places that policy_container are updated

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add some printlns in your function to print csp_list.to_string() and see if the directive is twice in it? I used that before to debug the contents of the list. You can also add this to the getter, so that you can always see when it is retrieved what the value is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

our problem stems from

"content-security-policy" => self.apply_csp_list(),
head.set_content_security_policy();
let candidates = node
.traverse_preorder(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.filter(|elem| elem.is::<HTMLMetaElement>())
.filter(|elem| {
elem.get_string_attribute(&local_name!("http-equiv"))
.to_ascii_lowercase() ==
*"content-security-policy"
})
.filter(|elem| {
elem.get_attribute(&ns!(), &local_name!("content"))
.is_some()
});
for meta in candidates {
if let Some(ref content) = meta.get_attribute(&ns!(), &local_name!("content")) {
let content = content.value();
let content_val = content.trim();
if !content_val.is_empty() {
let policies =
CspList::parse(content_val, PolicySource::Meta, PolicyDisposition::Enforce);
match csp_list {
Some(ref mut csp_list) => csp_list.append(policies),
None => csp_list = Some(policies),
}
}
}
}
doc.set_csp_list(csp_list);
on every meta element that we bind_to_tree it transverse the whole tree and gets all the connected metas.
And i think we are doing unnecessary work, since meta is the only valid element to carry the csp props we can just push the csp directly to doc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see the log

elomscansio@elomscansio:~/Desktop/projects/servo
$ ./mach test-wpt tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
No build type specified so assuming `--dev`.
Running WPT tests with /home/elomscansio/Desktop/projects/servo/target/debug/servo
 0:04.79 wptserve INFO Starting http server on http://127.0.0.1:8001
 0:04.84 wptserve INFO Starting http server on http://127.0.0.1:8002
 0:04.91 wptserve INFO Create socket on: (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:04.93 wptserve INFO Create socket on: (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:04.94 wptserve INFO Bind on: (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:04.94 wptserve INFO Bind on: (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:04.94 wptserve INFO Listen on: (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:04.94 wptserve INFO Listen on: (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:04.94 wptserve INFO Skip by failure: OSError(98, 'Address already in use')
 0:05.00 wptserve INFO Create socket on: (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:05.00 wptserve INFO Create socket on: (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:05.00 wptserve INFO Bind on: (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:05.00 wptserve INFO Bind on: (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:05.00 wptserve INFO Listen on: (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:05.00 wptserve INFO Listen on: (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, '', '', '')
 0:05.00 wptserve INFO Skip by failure: OSError(98, 'Address already in use')
 0:05.03 wptserve INFO Starting https server on https://127.0.0.1:8443
 0:05.04 wptserve INFO Starting https server on https://127.0.0.1:8446
 0:05.10 wptserve INFO Starting https server on https://127.0.0.1:8445
 0:05.12 wptserve INFO Starting https server on https://127.0.0.1:8444
 0:05.12 wptserve INFO Starting http2 server on https://127.0.0.1:9000
 0:05.13 wptserve INFO Starting http server on http://127.0.0.1:8000
 0:05.14 wptserve INFO Starting http server on http://127.0.0.1:8003
 0:05.57 SUITE_START: web-platform-test - running 1 tests
 0:05.58 INFO Using 1 child processes
 0:05.58 INFO Starting runner
 0:06.00 TEST_START: /trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html
 0:06.00 WARNING Got command init_succeeded in state running
 0:20.11 pid:7793 Full command: /home/elomscansio/Desktop/projects/servo/target/debug/servo --hard-fail -u Servo/wptrunner --ignore-certificate-errors --enable-experimental-web-platform-features http://web-platform.test:8000/trusted-types/TrustedTypePolicyFactory-createPolicy-cspTests-none.html --user-stylesheet /home/elomscansio/Desktop/projects/servo/resources/ahem.css --pref dom_testutils_enabled=true --pref dom_urlpattern_enabled=true --prefs-file /home/elomscansio/Desktop/projects/servo/resources/wpt-prefs.json --certificate-path /home/elomscansio/Desktop/projects/servo/tests/wpt/tests/tools/certs/cacert.pem
pid:7793 new csp list "trusted-types 'none'"
 0:20.12 pid:7793 csp list after update "trusted-types 'none'"
 0:20.12 pid:7793 new csp list "trusted-types 'none',connect-src 'none'"
 0:20.12 pid:7793 csp list before update "trusted-types 'none'"
 0:20.12 pid:7793 csp list after update "trusted-types 'none',trusted-types 'none',connect-src 'none'"
 0:20.69 pid:7793 

Copy link
Contributor Author

Choose a reason for hiding this comment

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

0:20.12 pid:7793 new csp list "trusted-types 'none',connect-src 'none'" the new csp list still includes what we've added before

Copy link
Contributor Author

@elomscansio elomscansio May 10, 2025

Choose a reason for hiding this comment

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

@jdm @TimvdLippe @PotatoCP I made few changes i'm not sure whether it complies with spec

@servo-wpt-sync
Copy link
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#52322).

@servo-wpt-sync
Copy link
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#52322).

});
}, "script without `abc` nonce is blocked by header CSP");
</script>
</body> No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's better to add newline at the end of the file. Same for tests/wpt/tests/content-security-policy/meta/meta-multiple-csp.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay

@servo-wpt-sync
Copy link
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#52322).

@servo-wpt-sync
Copy link
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#52322).

@servo-wpt-sync
Copy link
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#52322).

@elomscansio
Copy link
Contributor Author

ping

Copy link
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.

I don't have maintainer's rights on this repository, but from my perspective this PR looks good to me. Can you run a try run on your fork to see if this fixes any issues? You can do so with ./mach try wpt

@elomscansio
Copy link
Contributor Author

./mach try wpt

wow, Thanks

@TimvdLippe
Copy link
Contributor

I see you ran it successfully in https://github.com/elomscansio/servo/actions/runs/15079633979 Note that the single test failure has already been resolved on main. If you git rebase main your branch and run WPT again, it should pass.

Signed-off-by: Emmanuel Paul Elom <elomemmanuel007@gmail.com>
@servo-wpt-sync
Copy link
Collaborator

📝 Transplanted new upstreamable changes to existing upstream WPT pull request (web-platform-tests/wpt#52322).

@TimvdLippe
Copy link
Contributor

I found this WPT test that might be a duplicate https://github.com/web-platform-tests/wpt/blob/master/content-security-policy/meta/combine-header-and-meta-policies.sub.html I wonder why that doesn't pass with this PR?

@yezhizhen
Copy link
Member

yezhizhen commented Aug 29, 2025

@TimvdLippe This PR has been here for a while. I'm curious what's your opinion: is it still relevant?

@TimvdLippe
Copy link
Contributor

We are currently still failing https://wpt.fyi/results/content-security-policy/meta/combine-header-and-meta-policies.sub.html?product=servo so I think this fix is still relevant. Therefore, we don't need to introduce a new WPT test for this. Can you remove the new test and make the existing one pass?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CSP from <meta> overrides any existing CSP

6 participants