Skip to content

Enforce "at most once" semantics for scripts #392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
briansmith opened this issue May 6, 2019 · 8 comments
Open

Enforce "at most once" semantics for scripts #392

briansmith opened this issue May 6, 2019 · 8 comments
Labels
addition/proposal New features or enhancements

Comments

@briansmith
Copy link

I hypothesize that most don't consider the possibility that a script may be used as a gadget by an attacker. In particular, hash source and similar mechanisms don't work as one expects if the script is acccidentally non-idempotent (executing it a 2nd, 3rd, ..., nth time creates a problem that isn't caused by executing it the first time), or if the the safety of the script's logic depends on where exactly in the document it is inserted (e.g. before <body> vs after <body>, before DOMContentLoaded vs after, etc.).

One possibility would be to mark a hash source as "allow at most once". Then, if we put the whitelisted script as early in the document as possible, e.g. immediately after <meta charset> and <meta http-equiv=Content-Security-Policy> at the very top of the document, we can enforce that the hash source allows our own insertion of the script into the document, while blocking (virtually) all possible attempts to abuse the whitelisted script by attackers.

In particular, I propose to try to make this "allow at most once" semantics the one and only semantics for hash source. That is, instead of providing a way to opt into this proposed behavior, make it the default, and don't provide any way to opt out of it. This would technically be a breaking change but it probably doesn't actually break anything, since hash source probably isn't being used for scripts where the author intends the script to execute more than once.

(My goal in proposing this is to lock down the "Switch from <script> to ES6 imports" idea described in #243 (comment), but I think it probably is more generally useful.)

@annevk
Copy link
Member

annevk commented May 7, 2019

Could you explain how the "already started" flag does not enforce this? I wasn't able to easily reproduce.

@briansmith
Copy link
Author

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="script-src 'sha256-5jFwrAK0UV47oFbVg/iCCBbxD8X1w+QvoOUepu4C2YA='">
    <title>Title</title>
    <script>alert(1);</script>
</head>
<body>
    <script>alert(1);</script>
</body>
</html>

@annevk
Copy link
Member

annevk commented May 8, 2019

Ah thanks, it's about preventing multiple scripts from containing the same content, not about executing a single script twice.

I wonder if nonces are a better fit here. E.g., what if your script is at the end and the attacker injects theirs before yours?

@briansmith
Copy link
Author

I wonder if nonces are a better fit here.

I can't use nonces because the page isn't dynamically generated.

E.g., what if your script is at the end and the attacker injects theirs before yours?

Right. The idea is that one has to insert the script before most/all injection points.

The only goal is to prevent the surprising behavior that can occur when some scripts get inserted/executed twice when the author didn't expect it.

@dveditz
Copy link
Member

dveditz commented May 13, 2019

Another reason not to use nonces this way is that the general idea was for a page to have a single dynamic nonce and then use it for everything. It's trading a simpler content security policy for risk that the XSS is coming from inside the house (e.g. DOMXSS; individual hashes don't negate that risk).

@dveditz
Copy link
Member

dveditz commented May 13, 2019

Why is an attacker repeating a hashed inline script more dangerous than an attacker repeatedly including a whitelisted remote script? Are there any real-world cases where this has been a CSP bypass, or even a potential problem if a site had tried to use CSP? I'm not seeing a practical benefit equal to the added complication.

@briansmith
Copy link
Author

In the context of the system described at #243 (comment) where there is exactly one <script> to be allowed in the document, I think the effect requested here can be emulated by having that one hash-whitelisted script do this to disable all further scripts:

const meta = document.createElement("meta");
meta.httpEquiv = "Content-Security-Policy";
meta.content = "script-src 'none'";
document.head.appendChild(meta);

@briansmith
Copy link
Author

Why is an attacker repeating a hashed inline script more dangerous than an attacker repeatedly including a whitelisted remote script?

I agree that has a similar concern.

Note that I filed this issue to close a (small) hole in the idea in #243 (comment), which is to allow an application to replace all <script> with JS module imports, preventing any <script> except for one whitelisted (ideally by hash) loader script. That design prevents the "duplicate <script src>" form of the issue since <script src> would never be used.

Also, now CSP supports hash-source for external scripts when SRI is also used (in Chrome only, right now), and what I'm proposing here would also apply for that.

Are there any real-world cases where this has been a CSP bypass, or even a potential problem if a site had tried to use CSP? I'm not seeing a practical benefit equal to the added complication.

In everyday development there have been times where I've accidentally added the same (inline) script in a document. This caused problems because all events had duplicate handlers registered, and so I think it's pretty likely that people are likely to do the same with exploitable consequences.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements
Projects
None yet
Development

No branches or pull requests

4 participants