Can I ask a browser to not run – Ajedi32 Sep 19 '14 at 13:45

  • @Ajedi32 =:/... Well, what if you'd just do `content.replace("noscript", "");`? But by then `content.escapeHtmlSpecialChars();` would be easier... – 11684 Sep 19 '14 at 14:46
  • 1
    @11684 That's also super-easy to circumvent. ``. Point is, don't bother trying to implement this sort of XSS protection yourself. The *only* effective solution is to use proper escaping of user input, and then whitelist the tags you want to allow. (I won't go into detail, this topic has already been covered quite thoroughly elsewhere.) – Ajedi32 Sep 19 '14 at 14:57
  • 7 Answers7

    92

    YES, you can :-) The answer is: Content Security Policy (CSP).

    Most modern browsers support this flag, which tells the browser only to load JavaScript code from a trusted external file and disallow all internal JavaScript code! The only downside is, you can not use any inline JavaScript in your whole page (not only for a single <div>). Although there could be a workaround by dynamically including the div from an external file with a different security policy, but I'm not sure about that.

    But if you can change your site to load all JavaScript from external JavaScript files then you can disable inline JavaScript altogether with this header!

    Here is a nice tutorial with example: HTML5Rocks Tutorial

    If you can configure the server to send this HTTP-Header flag the world will be a better place!

    elixenide
    • 44,308
    • 16
    • 74
    • 100
    Falco
    • 3,287
    • 23
    • 26
    • 2
      +1 That's actually really cool, I had no idea that existed! (one note, your wiki link is to the German version directly) Here's the rundown on browser support: http://caniuse.com/#feat=contentsecuritypolicy – BrianH Sep 19 '14 at 14:39
    • 4
      Note that even if you do this, allowing unescaped user input on a page is still a bad idea. That would basically allow the user to change the page to look however they want. Even with all Content Security Policy (CSP) settings set to maximum (disallowing inline scripts, styles, etc), the user could still perform a cross-site request forgery (CSRF) attack using image srcs for GET requests, or tricking the user into clicking a form submit button for POST requests. – Ajedi32 Sep 19 '14 at 16:33
    • @Ajedi32 Of course you should always sanitize user inputs. But CSP can even set policies for GET requests like images or css, it will not only block them but even inform your server about it! – Falco Sep 19 '14 at 17:53
    • 1
      @Falco I read [the docs](https://developer.mozilla.org/en-US/docs/Web/Security/CSP/CSP_policy_directives), and my understanding was that you can only restrict those requests to a given domain, not a specific set of subpages on the domain. Presumably the domain you set would be the same one as your site, meaning that you would still be open to CSRF attacks. – Ajedi32 Sep 19 '14 at 17:58
    • @Falco Granted though, if your site is vulnerable to CSRF attacks that's a problem whether or not you're allowing users to put unescaped HTML on your site. Although, if your current method for preventing CSRF attacks involves checking the referrer header, that method wouldn't work against attacks originating from your own site. – Ajedi32 Sep 19 '14 at 18:14
    • @Ajedi32 you are right, if you provide the unescaped user-generated content on a page with the same subdomain as your relevant requests, CSP can not protect against it. I would contain the user-generated content in a sandboxed subdomain-zone and not allow any relevant GET/POST requests from there... AND of course santizie the input ;-) – Falco Sep 19 '14 at 19:11
    • 3
      @Falco Yeah, that's basically what StackExchange did with the new Stack Snippets feature: http://blog.stackoverflow.com/2014/09/introducing-runnable-javascript-css-and-html-code-snippets/ If you properly sanitize the input though, a seperate domain isn't necessary. – Ajedi32 Sep 19 '14 at 19:18
    • Just for anyone else coming here, CSP is very much still being worked on. Browser support comes and goes depending on updates. I've had Chrome break CSP completely at least twice until I went the report-only route. – AlbertEngelB Sep 23 '14 at 18:54
    14

    You can block JavaScript loaded by <script>, using beforescriptexecute event:

    <script>
      // Run this as early as possible, it isn't retroactive
      window.addEventListener('beforescriptexecute', function(e) {
        var el = e.target;
        while(el = el.parentElement)
          if(el.hasAttribute('data-no-js'))
            return e.preventDefault(); // Block script
      }, true);
    </script>
    
    <script>console.log('Allowed. Console is expected to show this');</script>
    <div data-no-js>
      <script>console.log('Blocked. Console is expected to NOT show this');</script>
    </div>

    Note that beforescriptexecute was defined in HTML 5.0 but has been removed in HTML 5.1. Firefox is the only major browser that implemented it.

    In case you are inserting an untrusted bunch of HTML in your page, be aware blocking scripts inside that element won't provide more security, because the untrusted HTML can close the sandboxed element, and thus the script will be placed outside and run.

    And this won't block things like <img onerror="javascript:alert('foo')" src="//" />.

    Oriol
    • 274,082
    • 63
    • 437
    • 513
    • The fiddle does not seem to work as expected. I should not be able to see the "blocked" part, right? – Salman A Sep 19 '14 at 19:17
    • @SalmanA Exactly. Probably, your browser doesn't support `beforescriptexecute` event. It works on Firefox. – Oriol Sep 19 '14 at 19:33
    • Probably because it's not working in Chrome, with the snippet as provided, though I see you've only just converted it to a snippet :-) – Mark Hurd May 28 '16 at 03:28
    • `beforescriptexecute` looks like it is not supported and will not be supported by most major browsers. https://developer.mozilla.org/en-US/docs/Web/Events/beforescriptexecute – Matt Pennington Nov 10 '16 at 19:42
    8

    Interesting question, I don't think it's possible. But even if it is, it sounds like it would be a hack.

    If the contents of that div are untrusted, then you need to escape the data on the server side before it is sent in the HTTP response and rendered in the browser.

    If you only want to remove <script> tags and allow other html tags, then just strip them out of the content and leave the rest.

    Look into XSS prevention.

    https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet

    cowls
    • 24,013
    • 8
    • 48
    • 78
    7

    JavaScript is executed "inline", i.e. in the order in which it appears in the DOM (if that wasn't the case, you could never be sure that some variable defined in a different script was visible when you used it for the first time).

    So that means in theory you could have a script at the beginning of the page (i.e. first <script> element) which looks through the DOM and removed all <script> elements and event handlers inside of your <div>.

    But the reality is more complex: DOM and script loading happens asynchronously. This means that the browser only guarantees that a script can see the part of the DOM which is before it (i.e. the header so far in our example). There are no guarantees for anything beyond (this is related to document.write()). So you might see the next script tag or maybe, you don't.

    You could latch to the onload event of the document - which would make sure you got the whole DOM - but at that time, malicious code could have already executed. Things get worse when other scripts manipulate the DOM, adding scripts there. So you would have to check for every change of the DOM, too.

    So @cowls solution (filtering on the server) is the only solution which can be made to work in all situations.

    Aaron Digulla
    • 321,842
    • 108
    • 597
    • 820
    1

    If you're looking to display JavaScript code in your browser:

    Using JavaScript and HTML, you'll have to use HTML entities to display the JavaScript code and avoiding this code to be executed. Here you can find the list of HTML entities:

    If you're using a server-side scripting language (PHP, ASP.NET, etc.), most probably, there's a function which would escape a string and convert the special characters into HTML entities. In PHP, you would use "htmlspecialchars()" or "htmlentities()". The latter covers all the HTML characters.

    If you're looking to display your JavaScript code in a nice way, then try one of the code highlighters:

    Wissam El-Kik
    • 2,469
    • 1
    • 17
    • 21
    1

    I've got a theory:

    • Wrap the specific part of the document inside a noscript tag.
    • Use DOM functions to discard all script tags inside the noscript tag then unwrap its contents.

    Proof of concept example:

    window.onload = function() {
        var noscripts = /* _live_ list */ document.getElementsByTagName("noscript"),
            memorydiv = document.createElement("div"),
            scripts = /* _live_ list */ memorydiv.getElementsByTagName("script"),
            i,
            j;
        for (i = noscripts.length - 1; i >= 0; --i) {
            memorydiv.innerHTML = noscripts[i].textContent || noscripts[i].innerText;
            for (j = scripts.length - 1; j >= 0; --j) {
                memorydiv.removeChild(scripts[j]);
            }
            while (memorydiv.firstChild) {
                noscripts[i].parentNode.insertBefore(memorydiv.firstChild, noscripts[i]);
            }
            noscripts[i].parentNode.removeChild(noscripts[i]);
        }
    };
    body { font: medium/1.5 monospace; }
    p, h1 { margin: 0; }
    <h1>Sample Content</h1>
    <p>1. This paragraph is embedded in HTML</p>
    <script>document.write('<p style="color: red;">2. This paragraph is generated by JavaScript</p>');</script>
    <p>3. This paragraph is embedded in HTML</p>
    <h1>Sample Content in No-JavaScript Zone</h1>
    <noscript>
        <p>1. This paragraph is embedded in HTML</p>
        <script>document.write('<p style="color: red;">2. This paragraph is generated by JavaScript</p>');</script>
        <p>3. This paragraph is embedded in HTML</p>
    </noscript>
    <noscript>
        <p>1. This paragraph is embedded in HTML</p>
        <script>document.write('<p style="color: red;">2. This paragraph is generated by JavaScript</p>');</script>
        <p>3. This paragraph is embedded in HTML</p>
    </noscript>
    Salman A
    • 262,204
    • 82
    • 430
    • 521
    • If the content in the div is untrusted, which i guess it is given the question. They could just close the ` – cowls Sep 21 '14 at 08:01
    • Yes, the proper solution is to fix the problem on the server side. What I am doing via JavaScript should better be done on the server side. – Salman A Sep 21 '14 at 08:55
    0

    If you want to re-enable script tags later on, my solution was to break the browser environment so that any script that runs will throw an error fairly early. However, it's not totally reliable, so you can't use it as a security feature.

    If you try to access global properties Chrome will throw an exception.

    setTimeout("Math.random()")
    // => VM116:25 Uncaught Error: JavaScript Execution Inhibited  
    

    I'm overwriting all overwritable properties on window, but you could also expand it to break other functionality.

    window.allowJSExecution = inhibitJavaScriptExecution();
    function inhibitJavaScriptExecution(){
    
        var windowProperties = {};
        var Object = window.Object
        var console = window.console
        var Error = window.Error
    
        function getPropertyDescriptor(object, propertyName){
            var descriptor = Object.getOwnPropertyDescriptor(object, propertyName);
            if (!descriptor) {
                return getPropertyDescriptor(Object.getPrototypeOf(object), propertyName);
            }
            return descriptor;
        }
    
        for (var propName in window){
            try {
                windowProperties[propName] = getPropertyDescriptor(window, propName)
                Object.defineProperty(window, propName, {
                    get: function(){
                        throw Error("JavaScript Execution Inhibited")
                    },
                    set: function(){
                        throw Error("JavaScript Execution Inhibited")
                    },
                    configurable: true
                })
            } catch (err) {}
        }
    
        return function allowJSExecution(){
            for (var propName in window){
                if (!(propName in windowProperties)) {
                    delete windowProperties[propName]
                }
            }
    
            for (var propName in windowProperties){
                try {
                    Object.defineProperty(window, propName, windowProperties[propName])
                } catch (err) {}
            }
        }
    }
    
    Matt Zeunert
    • 16,075
    • 6
    • 52
    • 78