Skip to content

Commit 534574f

Browse files
authored
Merge pull request github#10764 from pwntester/javascript_xss_improvements
JS: Consider other XSS unsafe content-types when reasoning about XSS vulnerabilities
2 parents c6b62c9 + 245be44 commit 534574f

File tree

5 files changed

+310
-24
lines changed

5 files changed

+310
-24
lines changed

javascript/ql/lib/semmle/javascript/security/dataflow/ReflectedXssCustomizations.qll

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ module ReflectedXss {
2525
* is to prevent us from flagging plain-text or JSON responses as vulnerable.
2626
*/
2727
class HttpResponseSink extends Sink instanceof Http::ResponseSendArgument {
28-
HttpResponseSink() { not exists(getANonHtmlHeaderDefinition(this)) }
28+
HttpResponseSink() { not exists(getAXssSafeHeaderDefinition(this)) }
2929
}
3030

3131
/**
32-
* Gets a HeaderDefinition that defines a non-html content-type for `send`.
32+
* DEPRECATED: Gets a HeaderDefinition that defines a non-html content-type for `send`.
3333
*/
34-
Http::HeaderDefinition getANonHtmlHeaderDefinition(Http::ResponseSendArgument send) {
34+
deprecated Http::HeaderDefinition getANonHtmlHeaderDefinition(Http::ResponseSendArgument send) {
3535
exists(Http::RouteHandler h |
3636
send.getRouteHandler() = h and
3737
result = nonHtmlContentTypeHeader(h)
@@ -42,13 +42,49 @@ module ReflectedXss {
4242
}
4343

4444
/**
45-
* Holds if `h` may send a response with a content type other than HTML.
45+
* DEPRECATED: Holds if `h` may send a response with a content type other than HTML.
4646
*/
47-
Http::HeaderDefinition nonHtmlContentTypeHeader(Http::RouteHandler h) {
47+
deprecated Http::HeaderDefinition nonHtmlContentTypeHeader(Http::RouteHandler h) {
4848
result = h.getAResponseHeader("content-type") and
4949
not exists(string tp | result.defines("content-type", tp) | tp.regexpMatch("(?i).*html.*"))
5050
}
5151

52+
/**
53+
* Gets a HeaderDefinition that defines a XSS safe content-type for `send`.
54+
*/
55+
Http::HeaderDefinition getAXssSafeHeaderDefinition(Http::ResponseSendArgument send) {
56+
exists(Http::RouteHandler h |
57+
send.getRouteHandler() = h and
58+
result = xssSafeContentTypeHeader(h)
59+
|
60+
// The HeaderDefinition affects a response sent at `send`.
61+
headerAffects(result, send)
62+
)
63+
}
64+
65+
/**
66+
* Gets a content-type that may lead to javascript code being executed in the browser.
67+
* ref: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#content-types
68+
*/
69+
string xssUnsafeContentType() {
70+
result =
71+
[
72+
"text/html", "application/xhtml+xml", "application/xml", "text/xml", "image/svg+xml",
73+
"text/xsl", "application/vnd.wap.xhtml+xml", "text/rdf", "application/rdf+xml",
74+
"application/mathml+xml", "text/vtt", "text/cache-manifest"
75+
]
76+
}
77+
78+
/**
79+
* Holds if `h` may send a response with a content type that is safe for XSS.
80+
*/
81+
Http::HeaderDefinition xssSafeContentTypeHeader(Http::RouteHandler h) {
82+
result = h.getAResponseHeader("content-type") and
83+
not exists(string tp | result.defines("content-type", tp) |
84+
tp.toLowerCase().matches(xssUnsafeContentType() + "%")
85+
)
86+
}
87+
5288
/**
5389
* Holds if a header set in `header` is likely to affect a response sent at `sender`.
5490
*/
@@ -61,6 +97,7 @@ module ReflectedXss {
6197
// There is no dominating header, and `header` is non-local.
6298
not isLocalHeaderDefinition(header) and
6399
not exists(Http::HeaderDefinition dominatingHeader |
100+
dominatingHeader.getAHeaderName() = "content-type" and
64101
dominatingHeader.getBasicBlock().(ReachableBasicBlock).dominates(sender.getBasicBlock())
65102
)
66103
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
import semmle.javascript.security.dataflow.ReflectedXssCustomizations
3+
4+
query predicate test_Xss(ReflectedXss::Sink sink, Http::ResponseSendArgument res) { sink = res }

javascript/ql/test/library-tests/frameworks/Express/src/express.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ app.post('/some/other/path', function(req, res) {
3434
app.get('/', require('./exportedHandler.js').handler);
3535

3636
function getHandler() {
37-
return function (req, res){}
37+
return function(req, res) { }
3838
}
3939
app.use(getHandler());
4040

4141
function getArrowHandler() {
42-
return (req, res) => f();
42+
return (req, res) => f();
4343
}
4444
app.use(getArrowHandler());
4545

@@ -49,3 +49,21 @@ app.post('/headers', function(req, res) {
4949
req.hostname;
5050
req.headers[config.headerName];
5151
});
52+
53+
app.get('/some/xss1', function(req, res) {
54+
res.header("Content-Type", "text/html");
55+
res.send(req.params.foo)
56+
foo(res);
57+
});
58+
59+
app.get('/some/xss2', function(req, res) {
60+
res.header("Content-Type", "application/xml");
61+
res.send(req.params.foo)
62+
foo(res);
63+
});
64+
65+
app.get('/some/non-xss1', function(req, res) {
66+
res.header("Content-Type", "text/plain");
67+
res.send(req.params.foo)
68+
foo(res);
69+
});

0 commit comments

Comments
 (0)