Skip to content

Commit 7afa755

Browse files
committed
JS: Add ajv error as source of ExceptionXss
1 parent 24199a5 commit 7afa755

File tree

7 files changed

+117
-7
lines changed

7 files changed

+117
-7
lines changed

javascript/ql/src/Security/CWE-079/ExceptionXss.qhelp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
<overview>
77
<p>
8-
Directly writing exceptions to a webpage without sanitization allows for a cross-site scripting
9-
vulnerability if the value of the exception can be influenced by a user.
8+
Directly writing error messages to a webpage without sanitization allows for a cross-site scripting
9+
vulnerability if parts of the error message can be influenced by a user.
1010
</p>
1111
</overview>
1212

@@ -27,6 +27,19 @@ leaving the website vulnerable to cross-site scripting.
2727
<sample src="examples/ExceptionXss.js" />
2828
</example>
2929

30+
<example>
31+
<p>
32+
This second example shows an input being validated using the JSON schema validator <code>ajv</code>,
33+
and in case of an error, the error message is sent directly back in the response.
34+
</p>
35+
<sample src="examples/ExceptionXssAjv.js" />
36+
<p>
37+
This is unsafe, because the error message can contain parts of the input.
38+
For example, the input <code>{'&lt;img src=x onerror=alert(1)&gt;': 'foo'}</code> will generate the error
39+
<code>data/&lt;img src=x onerror=alert(1)&gt; should be number</code>, causing reflected XSS.
40+
</p>
41+
</example>
42+
3043
<references>
3144
<li>
3245
OWASP:

javascript/ql/src/Security/CWE-079/ExceptionXss.ql

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@ import javascript
1515
import semmle.javascript.security.dataflow.ExceptionXss::ExceptionXss
1616
import DataFlow::PathGraph
1717

18+
/**
19+
* Gets a description of the source.
20+
*/
21+
string getSourceDescription(DataFlow::Node source) {
22+
result = source.(ErrorSource).getDescription()
23+
or
24+
not source instanceof ErrorSource and
25+
result = "Exception text"
26+
}
27+
1828
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
1929
where cfg.hasFlowPath(source, sink)
2030
select sink.getNode(), source, sink,
2131
"$@ is reinterpreted as HTML without escaping meta-characters.", source.getNode(),
22-
"Exception text"
32+
getSourceDescription(source.getNode())
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import express from 'express';
2+
import Ajv from 'ajv';
3+
4+
let app = express();
5+
let ajv = new Ajv();
6+
7+
ajv.addSchema({type: 'object', additionalProperties: {type: 'number'}}, 'pollData');
8+
9+
app.post('/polldata', (req, res) => {
10+
if (!ajv.validate('pollData', req.body)) {
11+
res.send(ajv.errorsText());
12+
}
13+
});

javascript/ql/src/semmle/javascript/JsonSchema.qll

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,31 @@ module JsonSchema {
3838
or
3939
result = ref().getMember(chainedMethod()).getReturn()
4040
}
41+
42+
/**
43+
* Gets an API node for a function produced by `new Ajv().compile()` or similar.
44+
*
45+
* Note that this does not include the instance method `new Ajv().validate` as its
46+
* signature is different.
47+
*/
48+
API::Node getAValidationFunction() {
49+
result = ref().getMember(["compile", "getSchema"]).getReturn()
50+
or
51+
result = ref().getMember("compileAsync").getPromised()
52+
}
53+
54+
/**
55+
* Gets an API node that refers to an error produced by this Ajv instance.
56+
*/
57+
API::Node getAValidationError() {
58+
exists(API::Node base |
59+
base = [ref(), getAValidationFunction()]
60+
|
61+
result = base.getMember("errors")
62+
or
63+
result = base.getMember("errorsText").getReturn()
64+
)
65+
}
4166
}
4267

4368
/** A call to the `validate` method of `ajv`. */
@@ -48,10 +73,7 @@ module JsonSchema {
4873
AjvValidationCall() {
4974
this = instance.ref().getMember("validate").getACall() and argIndex = 1
5075
or
51-
this = instance.ref().getMember(["compile", "getSchema"]).getReturn().getACall() and
52-
argIndex = 0
53-
or
54-
this = instance.ref().getMember("compileAsync").getPromised().getACall() and argIndex = 0
76+
this = instance.getAValidationFunction().getACall() and argIndex = 0
5577
}
5678

5779
override DataFlow::Node getInput() { result = getArgument(argIndex) }

javascript/ql/src/semmle/javascript/security/dataflow/ExceptionXss.qll

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,37 @@ module ExceptionXss {
130130
result = getCallbackErrorParam(pred)
131131
}
132132

133+
/**
134+
* A source of error values that is likely to contain unencoded user input.
135+
*/
136+
abstract class ErrorSource extends DataFlow::Node {
137+
/**
138+
* Gets a human-readable description of what type of error this refers to.
139+
*
140+
* The result should be captialized and usable in the context of a noun.
141+
*/
142+
abstract string getDescription();
143+
}
144+
145+
/**
146+
* An error produced by validating using `ajv`.
147+
*
148+
* Such an error can contain property names from the input if the
149+
* underlying schema uses `additionalProperties` or `propertyPatterns`.
150+
*
151+
* For example, an input of form `{"<img src=x onerror=alert(1)>": 45}` might produce the error
152+
* `data/<img src=x onerror=alert(1)> should be string`.
153+
*/
154+
private class JsonSchemaValidationError extends ErrorSource {
155+
JsonSchemaValidationError() {
156+
this = any(JsonSchema::Ajv::Instance i).getAValidationError().getAnImmediateUse()
157+
}
158+
159+
override string getDescription() {
160+
result = "JSON schema validation error"
161+
}
162+
}
163+
133164
/**
134165
* A taint-tracking configuration for reasoning about XSS with possible exceptional flow.
135166
* Flow labels are used to ensure that we only report taint-flow that has been thrown in
@@ -140,6 +171,9 @@ module ExceptionXss {
140171

141172
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
142173
source instanceof Xss::Shared::Source and label instanceof NotYetThrown
174+
or
175+
source instanceof ErrorSource and
176+
label.isTaint()
143177
}
144178

145179
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
nodes
2+
| ajv.js:11:18:11:33 | ajv.errorsText() |
3+
| ajv.js:11:18:11:33 | ajv.errorsText() |
4+
| ajv.js:11:18:11:33 | ajv.errorsText() |
25
| exception-xss.js:2:6:2:28 | foo |
36
| exception-xss.js:2:12:2:28 | document.location |
47
| exception-xss.js:2:12:2:28 | document.location |
@@ -87,6 +90,7 @@ nodes
8790
| exception-xss.js:182:19:182:23 | error |
8891
| exception-xss.js:182:19:182:23 | error |
8992
edges
93+
| ajv.js:11:18:11:33 | ajv.errorsText() | ajv.js:11:18:11:33 | ajv.errorsText() |
9094
| exception-xss.js:2:6:2:28 | foo | exception-xss.js:9:11:9:13 | foo |
9195
| exception-xss.js:2:6:2:28 | foo | exception-xss.js:15:9:15:11 | foo |
9296
| exception-xss.js:2:6:2:28 | foo | exception-xss.js:21:11:21:13 | foo |
@@ -169,6 +173,7 @@ edges
169173
| exception-xss.js:180:26:180:30 | error | exception-xss.js:182:19:182:23 | error |
170174
| exception-xss.js:180:26:180:30 | error | exception-xss.js:182:19:182:23 | error |
171175
#select
176+
| ajv.js:11:18:11:33 | ajv.errorsText() | ajv.js:11:18:11:33 | ajv.errorsText() | ajv.js:11:18:11:33 | ajv.errorsText() | $@ is reinterpreted as HTML without escaping meta-characters. | ajv.js:11:18:11:33 | ajv.errorsText() | JSON schema validation error |
172177
| exception-xss.js:11:18:11:18 | e | exception-xss.js:2:12:2:28 | document.location | exception-xss.js:11:18:11:18 | e | $@ is reinterpreted as HTML without escaping meta-characters. | exception-xss.js:2:12:2:28 | document.location | Exception text |
173178
| exception-xss.js:17:18:17:18 | e | exception-xss.js:2:12:2:28 | document.location | exception-xss.js:17:18:17:18 | e | $@ is reinterpreted as HTML without escaping meta-characters. | exception-xss.js:2:12:2:28 | document.location | Exception text |
174179
| exception-xss.js:23:18:23:18 | e | exception-xss.js:2:12:2:28 | document.location | exception-xss.js:23:18:23:18 | e | $@ is reinterpreted as HTML without escaping meta-characters. | exception-xss.js:2:12:2:28 | document.location | Exception text |
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import express from 'express';
2+
import Ajv from 'ajv';
3+
4+
let app = express();
5+
let ajv = new Ajv();
6+
7+
ajv.addSchema({type: 'object', additionalProperties: {type: 'number'}}, 'pollData');
8+
9+
app.post('/polldata', (req, res) => {
10+
if (!ajv.validate('pollData', req.body)) {
11+
res.send(ajv.errorsText()); // NOT OK
12+
}
13+
});

0 commit comments

Comments
 (0)