Skip to content
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

Scripts only in <head> #395

Closed
craigfrancis opened this issue May 21, 2019 · 11 comments

Comments

Projects
None yet
5 participants
@craigfrancis
Copy link

commented May 21, 2019

Taking some ideas from #392, and from the old/dead <plaintext> tag...

Considering most XSS exploits are found in the <body> (where most dynamic content is), and because websites can put all of their <script> tags in the <head> (ideally as async or defer), I would like to tell the browser that no <script> tag will be found after the <body> has started.

As an aside, the <body> is optional, so it's started even if the tag does not exist in the document.

@annevk

This comment has been minimized.

Copy link
Member

commented May 21, 2019

What about dynamic insertion?

@craigfrancis

This comment has been minimized.

Copy link
Author

commented May 21, 2019

Similar to how strict-dynamic allows dynamic insertion from scripts that have a valid hash/nonce, a <script> in the <head> should be allowed to do dynamic insertion.

Not that I use dynamic insertion (can I haz options to block all the things plz - where I'll probably be the only person in the world do that).

@annevk

This comment has been minimized.

Copy link
Member

commented May 21, 2019

Also, what about injecting <iframe>, event handlers, or equivalent? This doesn't seem really robust to me.

@craigfrancis

This comment has been minimized.

Copy link
Author

commented May 21, 2019

Similar to 'strict-dynamic', blessed scripts (in the <head>) should be allowed to add any <iframe>, <script>, event handlers, etc - as I'm focusing on the most likely location for an XSS, which is in the <body> (where you're most likely to find an XSS vulnerability).

And while most XSS issues should be covered by CSP as normal; an attacker might be able to create a bypass in the form of repeating a <script> with a white-listed hash, or guess a poorly implemented nonce, or including a bad <script> from a white-listed location (e.g. JSONP)... so this is just seeing if we could provide an extra barrier.

That said, an attacker adding an <iframe> in the <body> could could break this.

e.g. <iframe srcdoc="EVIL" sandbox="allow-same-origin"> introduces its own <head>.

I'm hesitant to suggest same-origin iframes will have to have scripts in their <head> blocked, as while that won't effect me, I suspect it will be something that's confusing, and too limiting.

Any suggestions? if not, it's probably best closing this.

@Malvoz

This comment has been minimized.

Copy link

commented May 27, 2019

I have a limited understanding of all of this, but to me it looks like this is something that trusted types could restrict. Am I wrong?

@craigfrancis

This comment has been minimized.

Copy link
Author

commented May 27, 2019

@Malvoz, not really, CSP is about the content that can be loaded; whereas Trusted Types limits the JavaScript methods/APIs that it can used after it has been loaded (e.g. it restricts the use of innerHTML).

@dveditz

This comment has been minimized.

Copy link

commented Jun 3, 2019

XSS in the <head> is a thing (the title tag was a common vector for a while, and DOMXSS still happens there), but assuming your <head> is perfect could you close out your head with a CSP of script-src 'none'; ? Would block dynamic insertion, but is that really a bad thing?

@craigfrancis

This comment has been minimized.

Copy link
Author

commented Jun 6, 2019

Good thinking @dveditz,

I can set a normal CSP header, then just after the last valid <script> tag in the <head>, I can add a <meta> tag to add another CSP, which will stop further scripts being included.

If I do have an XSS in the body, it means attackers can't add any <script> tags, e.g.

<?php
  header("Content-Security-Policy: default-src 'none'; script-src https://example.com/js/");
?>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <script src="https://example.com/js/responsive.js"></script>
  <meta http-equiv="Content-Security-Policy" content="script-src 'none'" />
  <title>XSS Example</title>
</head>
<body>
  <p>Hi <?= $_GET['name'] ?></p>
</body>
</html>
@craigfrancis

This comment has been minimized.

Copy link
Author

commented Jun 15, 2019

This technique doesn't work for MS Edge 17.17134 (and maybe earlier) when using normal "text/html".

The CSP3 spec says "policies in <meta> elements are not applied to content which precedes them", which implies that it should work, but MS Edge doesn't always follow this rule.


Interestingly, this technique works in Edge when using "application/xhtml+xml".

It will also continue to work if you change to "text/html" and press the Refresh button - as Edge will continue to use the XML parser (more info).

And as a second Edge 17 bug, when adding this <meta> tag, which only adds "script-src 'none'", the style sheets aren't used when printing the page.

I will now skip this <meta> tag if the User-Agent header contains "Edge/", as the new Chromium version seems to be fine, and uses "Edg/".

@briansmith

This comment has been minimized.

Copy link

commented Jun 15, 2019

@craigfrancis What if you insert the <meta> dynamically with a script, as suggested in #243 (comment)?

@craigfrancis

This comment has been minimized.

Copy link
Author

commented Jun 17, 2019

That's interesting @briansmith, adding it dynamically in MS Edge 17, inline scripts that followed it are now blocked - good; but external scripts are still allowed (most of the time) - not good, but at least all the legitimate scripts still work.


When I say "most of the time", if the evil XSS:

  • Re-included a file, that's always allowed.
  • Included a different file, that's still white-listed by the first CSP, then it seems to be a race condition; e.g. on my local machine, with content cached, Edge allowed it most of the time.

So your approach can be used for all browsers (while Edge 17 is wrong, at least it no longer blocks all JavaScript).

But from a purist point of view, it does introduce inline JavaScript (I don't want to put this in an external file, as I want to load all my JavaScript asynchronously, for performance reasons).


Test Script:

<?php
  $nonce = base64_encode(random_bytes(30));
  header('Content-Type: ' . (false ? 'application/xhtml+xml' : 'text/html') . '; charset=UTF-8');
  header("Content-Security-Policy: default-src 'none'; script-src https://example.com/js/ 'nonce-" . $nonce . "'");
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>

  <title>MS Edge Test</title>

  <!-- Good Scripts -->
  <script src="https://example.com/js/external1.js"></script>
  <script type="text/javascript" nonce="<?= htmlentities($nonce) ?>">
  	console.log('Allowed Inline1');
  </script>

  <script type="text/javascript" nonce="<?= htmlentities($nonce) ?>">
    var meta = document.createElement('meta');
    meta.httpEquiv = 'Content-Security-Policy';
    meta.content = "script-src 'none'";
    document.head.appendChild(meta);
    // document.write('<meta http-equiv="Content-Security-Policy" content="script-src \'none\'" />');
  </script>
  <!-- <meta http-equiv="Content-Security-Policy" content="script-src 'none'" /> -->
  
  <!-- Bad Scripts, from XSS Exploit -->
  <script src="https://example.com/js/external2.js"></script>
  <script type="text/javascript" nonce="<?= htmlentities($nonce) ?>">
  	console.log('Allowed Inline2');
  </script>

</head>
<body>
  <!-- </div> -->
  <p><a href="./">Reload</a></p>
  <p id="output"></p>
</body>
</html>

Results for appendChild:

  • Allowed External1
  • Allowed Inline1
  • Allowed External2
  • CSP14321: Resource violated directive 'script-src 'none'' in meta http-equiv="Content-Security-Policy">: inline script, in https://example.com/js/ at line 21 column 82. Resource will be blocked.

Results for document.write:

  • Allowed External1
  • Allowed Inline1
  • Allowed External2
  • Allowed Inline2

Results for <meta>:

  • CSP14312: Resource violated directive 'script-src 'none'' in meta http-equiv="Content-Security-Policy">: https://example.com/js/external1.js. Resource will be blocked.
  • CSP14312: Resource violated directive 'script-src 'none'' in meta http-equiv="Content-Security-Policy">: https://example.com/js/external2.js. Resource will be blocked.
  • CSP14321: Resource violated directive 'script-src 'none'' in meta http-equiv="Content-Security-Policy">: inline script, in https://example.com/js/ at line 7 column 82. Resource will be blocked.
  • CSP14321: Resource violated directive 'script-src 'none'' in meta http-equiv="Content-Security-Policy">: inline script, in https://example.com/js/ at line 14 column 82. Resource will be blocked.

In this last one, note how the order is wrong, the external files are listed first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.