XSS (Cross Site Scripting)
CSRF (Cross Site Request Forgery)
And how to prevent (or fix) them.
Cross Site Scripting
XSS is an injection attack, driven by user-controlled inputs
Potentially, a user can place arbitrary
HTML
and/or
JavaScript
on to your page!
An example
<h1>Hello <%- user.name %>, welcome to <%- site.name %></h1>
Where <%- %>
is an Interpolation operator for a Template Slot.
What happens if someone updates my profile and changes my name from
“Jeremy”
to
“</h1><script>window.location='https://evil.com'</script>”?
<h1>Hello <%- user.name %>, welcome to <%- site.name %></h1>
... is rendered as ...
<h1>Hello </h1>
<script>window.location='https://evil.com'</script>,
welcome to My Awesome Site</h1>
Best case: Compare against an Allow List of known-good values
e.g.
var HANDEDNESS = ['Lefty','Righty','Ambidexterous','Other'];
Not everything can be Validated against an Allow List
Human names don't fit into a convenient list :(
Instead, you might say "anything but <>
" to at least exclude HTML tags.
Goal: Prevent user-controlled data from breaking out of its context.
Means: Convert unsafe markup to safe markup.
HTML Entity-encoding takes markup characters and turns them into display characters.
Character | Encoding |
---|---|
< | < |
> | > |
' | ' |
" | " or " |
& | & or & |
(Insert all 65536 JavaScript UTF-16 code-points here)
Basically, entity-encode characters not in this RegExp set:
[\t\n\v\f\r ,\.0-9A-Z_a-z\-\u00A0-\uFFFF]
source: secure-filters
Change ...
<h1>Hello <%- user.name %>, welcome to <%- site.name %></h1>
... to ...
<h1>Hello <%= user.name %>, welcome to <%- site.name %></h1>
Where <%= %>
is an Escaping operator for a Template Slot.
This changes the bad output from...
<h1>Hello </h1>
<script>window.location='https://evil.com'</script>,
welcome to My Awesome Site</h1>
... to the safe (entity-encoded) ...
<h1>Hello </h1>
<script>window.location='https://evil.com'</script>,
welcome to My Awesome Site</h1>
So... I just have to worry about escaping HTML?
There's more to it than HTML entity-encoding!
<script>
var foo = <%- someJSON %>;
</script>
+
{ someJSON: JSON.stringify("</script><script>alert('boom');//") }
=
<script>
var foo = "</script><script>alert('boom');//";
</script>
In strings, things like <
become \x3C
, etc.
<script>
var foo = "</script><script>alert('boom');//";
</script>
... becomes ...
<script>
var foo = "\x3C/script\x3E\x3Cscript\x3Ealert('boom');//";
</script>
innerHTML
<script>
var userName = "Jeremy\x3Cscript\x3Ealert('boom')\x3C/script\x3E";
element.innerHTML = "<span>"+userName+"</span>";
</script>
<a href="/show?user=<%= userId %>">...</a>;
+
{ userId: "42&user=666" }
=
<a href="/show?user=42&user=666">...</a>;
The server sees https://example.com/show?user=42&user=666
, so maybe shows user 666 now?
Convert unsafe characters to %XX
UTF-8 octets.
E.g. &
to %26
<a href="/show?user=42%26user=666">...</a>;
Luckily, parseInt("42&user=666")
evaluates to 42
.
Are there any tools to help me with Sanitization?
www.npmjs.org/package/secure-filters
Works in node.js and browsers, includes EJS support
<script>
var config = <%-: config |jsObj%>;
var userId = parseInt('<%-: userId |js%>',10);
</script>
<a href="/welcome/<%-: userId |uri%>">Welcome <%-: userName |html%></a>
<a href="javascript:activate('<%-: userId |jsAttr%>')">
Click here to activate</a>
Can use these as regular functions too
docs.phalconphp.com/en/latest/reference/escaper.html
Good selection of output filters
<title><?php echo $e->escapeHtml($maliciousTitle) ?></title>
<style type="text/css">
.<?php echo $e->escapeCss($className) ?> {
font-family : "<?php echo $e->escapeCss($fontName) ?>";
}
</style>
<div class='<?php echo $e->escapeHtmlAttr($className) ?>'>hello</div>
<script>var some = '<?php echo $e->escapeJs($javascriptText) ?>'</script>
docs.angularjs.org/api/ng/service/$sce
The {{ }}
operator and ng-
attributes are context-aware!
facebook.github.io/react/docs/jsx-in-depth.html
DOM manipulation macros are available without JSX:
var link = React.DOM.a({href: 'https://example.com/'}, 'React');
Or, conveniently in JSX:
var link = <a href="https://example.com/">React</a>;
Has APIs for escaping output, as well as input-validation helpers, anti-CSRF and more.
html/template
Based on EcmaScript Harmony "Quasis" (a.k.a. Tagged Template Strings)
<a href="/search?q={{.}}">{{.}}</a>
... is compiled to mean ...
<a href="/search?q={{. | urlquery}}">{{. | html}}</a>
Should I sanitize inputs?
Sanitizing input permanently modifies the data.
Sanitization is fairly cheap and highly cacheable!
Validation can't cover everything...
... and Sanitization can't catch all the cases...
(but you should still do them!)
... we needed something more!
Pages define an Allow-List of what features (and their Origins) are permissible.
Serve as a HTTP header (or use a <meta>
HTML tag)
Content-Security-Policy:
default-src 'none';
connect-src ws-and-xhr.example.com;
font-src https://fonts.googleapis.com;
frame-src 'self';
media-src youtube.com, ytimg.com;
script-src https://example-cdn.com, https://cloudflare.com;
style-src https://example-cdn.com;
Remember this?
<h1>Hello </h1>
<script>window.location='https://evil.com'</script>,
welcome to My Awesome Site</h1>
It could have been prevented with restricting scripts sourced from the same Origin:
Content-Security-Policy: script-src 'self'
With script-src 'self'
, all unknown script sources are also blocked:
<!-- allowed by CSP: -->
<script src="/main.js"></script>
<!-- blocked by CSP: -->
<script src="https://evil.com/attack.js"></script>
Consequently, to allow inline script blocks, instead of ...
Content-Security-Policy: script-src 'self'
... we'd need to say ...
Content-Security-Policy: script-src 'self', 'unsafe-inline'
Are there any tools to help me with CSP?
require('helmet')
Connect middleware that does CSP and more!
var helmet = require('helmet');
var app = express(); // or connect
app.use(helmet.csp());
app.use(helmet.xframe('deny'));
app.use(helmet.contentTypeOptions());
Neat tool using Report-Only mode to dynamically help you form a valid CSP header.
Just be aware it does send a list of all included scripts/fonts/etc to do that analysis
Cross-Site Request Forgery
CSRF exploits the fact that you are logged-in to some other site.
For example,
https://example.com
http://evil.com
Say evil.com
has the following HTML:
<title>Welcome to Evil.com</title>
<script src="https://example.com/api/inviteAdmin?email=hacker@evil.com"></script>
Even though you're visiting evil.com
,
you're still authenticated with example.com
!
Assert that the user intended to do this action.
E.g.
The user was on my website ...
... then, they clicked submit on a form ...
... therefore, this isn't a Cross-Site Forgery
"The user was on my website ..."
=
Put into any Forms a unique, secret Anti-CSRF token that's tied to their login-cookie.
"... then they clicked submit on a form ..."
=
Actions that change application state should be POST
/PUT
/PATCH
/DELETE
(consistent with the REST Architectural Style)
Note: that POST
/etc. on its own is not enough to stop CSRF based on XHR!
"... therefore, this isn't a Cross-Site Forgery"
=
Validate the Anti-CSRF token, which since it was a secret, the attacker can't know.
Note: HTTP isn't very good at keeping secrets, so consider the importance of HTTPS.
Assume it's running a simple Express 3.x node.js server with EJS templates.
app.get('/api/inviteAdmin', handlerFn);
... change this to ...
app.post('/api/inviteAdmin', handlerFn);
app.use(connect.session());
app.use(connect.csrf());
... then to access the token ...
var token = req.csrfToken();
res.render('template', { _csrf: token });
<form method="GET" action="/api/inviteAdmin">
<input type="email" name="email">
... change to use POST
and consume anti-CSRF token ...
<form method="POST" action="/api/inviteAdmin">
<input type="hidden" name="_csrf" value="<%- _csrf %>">
<input type="email" name="email">
Are there ways to verify intent without a CSRF token?
Idea: ask the user for confirmation (just make sure the confirmation isn't CSRF-attackable and is server-controlled)
Idea: For really sensitive operations, re-prompting for a password is good, especially for long-lived sessions
You too can prevent forest CSRF fires!
POST
/PUT
/etc. instead of GET
.