Skip to content

Commit db2720e

Browse files
committed
JS: Initial model of Response
1 parent 9ebaac8 commit db2720e

File tree

5 files changed

+166
-14
lines changed

5 files changed

+166
-14
lines changed

javascript/ql/lib/javascript.qll

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ import semmle.javascript.frameworks.UriLibraries
136136
import semmle.javascript.frameworks.Vue
137137
import semmle.javascript.frameworks.Vuex
138138
import semmle.javascript.frameworks.Webix
139+
import semmle.javascript.frameworks.WebResponse
139140
import semmle.javascript.frameworks.WebSocket
140141
import semmle.javascript.frameworks.XmlParsers
141142
import semmle.javascript.frameworks.xUnit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Models the `Request` and `Response` objects from the Web standards.
3+
*/
4+
5+
private import javascript
6+
7+
/** Treats `Response` as an entry point for API graphs. */
8+
private class ResponseEntryPoint extends API::EntryPoint {
9+
ResponseEntryPoint() { this = "global.Response" }
10+
11+
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("Response") }
12+
}
13+
14+
/** Treats `Headers` as an entry point for API graphs. */
15+
private class HeadersEntryPoint extends API::EntryPoint {
16+
HeadersEntryPoint() { this = "global.Headers" }
17+
18+
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("Headers") }
19+
}
20+
21+
/**
22+
* A call to the `Response` constructor.
23+
*/
24+
private class ResponseCall extends API::InvokeNode {
25+
ResponseCall() { this = any(ResponseEntryPoint e).getANode().getAnInstantiation() }
26+
}
27+
28+
/**
29+
* A call to the `Headers` constructor.
30+
*/
31+
private class HeadersCall extends API::InvokeNode {
32+
HeadersCall() { this = any(HeadersEntryPoint e).getANode().getAnInstantiation() }
33+
}
34+
35+
/**
36+
* The `headers` in `new Response(body, { headers })`
37+
*/
38+
private class ResponseArgumentHeaders extends Http::HeaderDefinition {
39+
private ResponseCall response;
40+
private API::Node headerNode;
41+
42+
ResponseArgumentHeaders() {
43+
headerNode = response.getParameter(1).getMember("headers") and
44+
this = headerNode.asSink()
45+
}
46+
47+
ResponseCall getResponse() { result = response }
48+
49+
/**
50+
* Gets a call to `new Headers()` that is passed as the headers to this call.
51+
*/
52+
private HeadersCall getHeadersCall() { headerNode.refersTo(result.getReturn()) }
53+
54+
/**
55+
* Gets an object whose properties are interpreted as headers, such as `{'content-type': 'foo'}`.
56+
*/
57+
private API::Node getAPlainHeaderObject() {
58+
// new Response(body, {...})
59+
result = headerNode
60+
or
61+
// new Response(body, new Headers({...}))
62+
result = this.getHeadersCall().getParameter(0)
63+
}
64+
65+
private API::Node getHeaderNode(string headerName) {
66+
exists(string prop |
67+
result = this.getAPlainHeaderObject().getMember(prop) and
68+
headerName = prop.toLowerCase()
69+
)
70+
or
71+
exists(API::CallNode append |
72+
append = this.getHeadersCall().getReturn().getMember(["append", "set"]).getACall() and
73+
headerName = append.getArgument(0).getStringValue().toLowerCase() and
74+
result = append.getParameter(1)
75+
)
76+
}
77+
78+
override predicate defines(string headerName, string headerValue) {
79+
this.getHeaderNode(headerName).getAValueReachingSink().getStringValue() = headerValue
80+
}
81+
82+
override string getAHeaderName() { exists(this.getHeaderNode(result)) }
83+
84+
override Http::RouteHandler getRouteHandler() { none() }
85+
}
86+
87+
/**
88+
* Data passed as the body in `new Response(body, ...)`.
89+
*/
90+
private class ResponseSink extends Http::ResponseSendArgument {
91+
private ResponseCall response;
92+
93+
ResponseSink() { this = response.getArgument(0) }
94+
95+
override Http::RouteHandler getRouteHandler() { none() }
96+
}

javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected

+42
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@
4040
| partial.js:28:14:28:18 | x + y | partial.js:31:47:31:53 | req.url | partial.js:28:14:28:18 | x + y | Cross-site scripting vulnerability due to a $@. | partial.js:31:47:31:53 | req.url | user-provided value |
4141
| partial.js:37:14:37:18 | x + y | partial.js:40:43:40:49 | req.url | partial.js:37:14:37:18 | x + y | Cross-site scripting vulnerability due to a $@. | partial.js:40:43:40:49 | req.url | user-provided value |
4242
| promises.js:6:25:6:25 | x | promises.js:5:44:5:57 | req.query.data | promises.js:6:25:6:25 | x | Cross-site scripting vulnerability due to a $@. | promises.js:5:44:5:57 | req.query.data | user-provided value |
43+
| response-object.js:9:18:9:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:9:18:9:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
44+
| response-object.js:10:18:10:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:10:18:10:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
45+
| response-object.js:11:18:11:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:11:18:11:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
46+
| response-object.js:13:18:13:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:13:18:13:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
47+
| response-object.js:14:18:14:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:14:18:14:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
48+
| response-object.js:16:18:16:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:16:18:16:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
49+
| response-object.js:17:18:17:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:17:18:17:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
50+
| response-object.js:20:18:20:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:20:18:20:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
51+
| response-object.js:23:18:23:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:23:18:23:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
52+
| response-object.js:26:18:26:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:26:18:26:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
53+
| response-object.js:30:18:30:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:30:18:30:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
54+
| response-object.js:34:18:34:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:34:18:34:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
55+
| response-object.js:38:18:38:21 | data | response-object.js:7:18:7:25 | req.body | response-object.js:38:18:38:21 | data | Cross-site scripting vulnerability due to a $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
4356
| tst2.js:7:12:7:12 | p | tst2.js:6:9:6:9 | p | tst2.js:7:12:7:12 | p | Cross-site scripting vulnerability due to a $@. | tst2.js:6:9:6:9 | p | user-provided value |
4457
| tst2.js:8:12:8:12 | r | tst2.js:6:12:6:15 | q: r | tst2.js:8:12:8:12 | r | Cross-site scripting vulnerability due to a $@. | tst2.js:6:12:6:15 | q: r | user-provided value |
4558
| tst2.js:18:12:18:12 | p | tst2.js:14:9:14:9 | p | tst2.js:18:12:18:12 | p | Cross-site scripting vulnerability due to a $@. | tst2.js:14:9:14:9 | p | user-provided value |
@@ -149,6 +162,20 @@ edges
149162
| promises.js:5:36:5:42 | [post update] resolve [resolve-value] | promises.js:5:16:5:22 | resolve [Return] [resolve-value] | provenance | |
150163
| promises.js:5:44:5:57 | req.query.data | promises.js:5:36:5:42 | [post update] resolve [resolve-value] | provenance | |
151164
| promises.js:6:11:6:11 | x | promises.js:6:25:6:25 | x | provenance | |
165+
| response-object.js:7:11:7:25 | data | response-object.js:9:18:9:21 | data | provenance | |
166+
| response-object.js:7:11:7:25 | data | response-object.js:10:18:10:21 | data | provenance | |
167+
| response-object.js:7:11:7:25 | data | response-object.js:11:18:11:21 | data | provenance | |
168+
| response-object.js:7:11:7:25 | data | response-object.js:13:18:13:21 | data | provenance | |
169+
| response-object.js:7:11:7:25 | data | response-object.js:14:18:14:21 | data | provenance | |
170+
| response-object.js:7:11:7:25 | data | response-object.js:16:18:16:21 | data | provenance | |
171+
| response-object.js:7:11:7:25 | data | response-object.js:17:18:17:21 | data | provenance | |
172+
| response-object.js:7:11:7:25 | data | response-object.js:20:18:20:21 | data | provenance | |
173+
| response-object.js:7:11:7:25 | data | response-object.js:23:18:23:21 | data | provenance | |
174+
| response-object.js:7:11:7:25 | data | response-object.js:26:18:26:21 | data | provenance | |
175+
| response-object.js:7:11:7:25 | data | response-object.js:30:18:30:21 | data | provenance | |
176+
| response-object.js:7:11:7:25 | data | response-object.js:34:18:34:21 | data | provenance | |
177+
| response-object.js:7:11:7:25 | data | response-object.js:38:18:38:21 | data | provenance | |
178+
| response-object.js:7:18:7:25 | req.body | response-object.js:7:11:7:25 | data | provenance | |
152179
| tst2.js:6:7:6:30 | p | tst2.js:7:12:7:12 | p | provenance | |
153180
| tst2.js:6:7:6:30 | r | tst2.js:8:12:8:12 | r | provenance | |
154181
| tst2.js:6:9:6:9 | p | tst2.js:6:7:6:30 | p | provenance | |
@@ -332,6 +359,21 @@ nodes
332359
| promises.js:5:44:5:57 | req.query.data | semmle.label | req.query.data |
333360
| promises.js:6:11:6:11 | x | semmle.label | x |
334361
| promises.js:6:25:6:25 | x | semmle.label | x |
362+
| response-object.js:7:11:7:25 | data | semmle.label | data |
363+
| response-object.js:7:18:7:25 | req.body | semmle.label | req.body |
364+
| response-object.js:9:18:9:21 | data | semmle.label | data |
365+
| response-object.js:10:18:10:21 | data | semmle.label | data |
366+
| response-object.js:11:18:11:21 | data | semmle.label | data |
367+
| response-object.js:13:18:13:21 | data | semmle.label | data |
368+
| response-object.js:14:18:14:21 | data | semmle.label | data |
369+
| response-object.js:16:18:16:21 | data | semmle.label | data |
370+
| response-object.js:17:18:17:21 | data | semmle.label | data |
371+
| response-object.js:20:18:20:21 | data | semmle.label | data |
372+
| response-object.js:23:18:23:21 | data | semmle.label | data |
373+
| response-object.js:26:18:26:21 | data | semmle.label | data |
374+
| response-object.js:30:18:30:21 | data | semmle.label | data |
375+
| response-object.js:34:18:34:21 | data | semmle.label | data |
376+
| response-object.js:38:18:38:21 | data | semmle.label | data |
335377
| tst2.js:6:7:6:30 | p | semmle.label | p |
336378
| tst2.js:6:7:6:30 | r | semmle.label | r |
337379
| tst2.js:6:9:6:9 | p | semmle.label | p |

javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected

+13
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@
3838
| partial.js:28:14:28:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:31:47:31:53 | req.url | user-provided value |
3939
| partial.js:37:14:37:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:40:43:40:49 | req.url | user-provided value |
4040
| promises.js:6:25:6:25 | x | Cross-site scripting vulnerability due to $@. | promises.js:5:44:5:57 | req.query.data | user-provided value |
41+
| response-object.js:9:18:9:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
42+
| response-object.js:10:18:10:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
43+
| response-object.js:11:18:11:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
44+
| response-object.js:13:18:13:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
45+
| response-object.js:14:18:14:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
46+
| response-object.js:16:18:16:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
47+
| response-object.js:17:18:17:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
48+
| response-object.js:20:18:20:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
49+
| response-object.js:23:18:23:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
50+
| response-object.js:26:18:26:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
51+
| response-object.js:30:18:30:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
52+
| response-object.js:34:18:34:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
53+
| response-object.js:38:18:38:21 | data | Cross-site scripting vulnerability due to $@. | response-object.js:7:18:7:25 | req.body | user-provided value |
4154
| tst2.js:7:12:7:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:6:9:6:9 | p | user-provided value |
4255
| tst2.js:8:12:8:12 | r | Cross-site scripting vulnerability due to $@. | tst2.js:6:12:6:15 | q: r | user-provided value |
4356
| tst2.js:18:12:18:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:14:9:14:9 | p | user-provided value |

javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/response-object.js

+14-14
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,36 @@ const express = require('express');
44
// in isolation from the more complicated http frameworks.
55

66
express().get('/foo', (req) => {
7-
const data = req.body; // $ MISSING: Source
7+
const data = req.body; // $ Source
88

9-
new Response(data); // $ MISSING: Alert
10-
new Response(data, {}); // $ MISSING: Alert
11-
new Response(data, { headers: null }); // $ MISSING: Alert
9+
new Response(data); // $ Alert
10+
new Response(data, {}); // $ Alert
11+
new Response(data, { headers: null }); // $ Alert
1212

13-
new Response(data, { headers: { 'content-type': 'text/plain'}});
14-
new Response(data, { headers: { 'content-type': 'text/html'}}); // $ MISSING: Alert
13+
new Response(data, { headers: { 'content-type': 'text/plain'}}); // $ SPURIOUS: Alert
14+
new Response(data, { headers: { 'content-type': 'text/html'}}); // $ Alert
1515

16-
new Response(data, { headers: { 'Content-Type': 'text/plain'}});
17-
new Response(data, { headers: { 'Content-Type': 'text/html'}}); // $ MISSING: Alert
16+
new Response(data, { headers: { 'Content-Type': 'text/plain'}}); // $ SPURIOUS: Alert
17+
new Response(data, { headers: { 'Content-Type': 'text/html'}}); // $ Alert
1818

1919
const headers1 = new Headers({ 'content-type': 'text/plain'});
20-
new Response(data, { headers: headers1 });
20+
new Response(data, { headers: headers1 }); // $ SPURIOUS: Alert
2121

2222
const headers2 = new Headers({ 'content-type': 'text/html'});
23-
new Response(data, { headers: headers2 }); // $ MISSING: Alert
23+
new Response(data, { headers: headers2 }); // $ Alert
2424

2525
const headers3 = new Headers();
26-
new Response(data, { headers: headers3 }); // $ MISSING: Alert
26+
new Response(data, { headers: headers3 }); // $ Alert
2727

2828
const headers4 = new Headers();
2929
headers4.set('content-type', 'text/plain');
30-
new Response(data, { headers: headers4 });
30+
new Response(data, { headers: headers4 }); // $ SPURIOUS: Alert
3131

3232
const headers5 = new Headers();
3333
headers5.set('content-type', 'text/html');
34-
new Response(data, { headers: headers5 }); // $ MISSING: Alert
34+
new Response(data, { headers: headers5 }); // $ Alert
3535

3636
const headers6 = new Headers();
3737
headers6.set('unrelated-header', 'text/plain');
38-
new Response(data, { headers: headers6 }); // $ MISSING: Alert
38+
new Response(data, { headers: headers6 }); // $ Alert
3939
});

0 commit comments

Comments
 (0)