Skip to content

Commit d91218e

Browse files
author
Max Schaefer
authored
Merge pull request #10 from asger-semmle/json-parsers
JavaScript: Add model of JSON parsers
2 parents e8df86e + 5e88eeb commit d91218e

File tree

12 files changed

+194
-38
lines changed

12 files changed

+194
-38
lines changed

change-notes/1.18/analysis-javascript.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@
1414
- [crypto-js](https://github.com/https://github.com/brix/crypto-js)
1515
- [express-jwt](https://github.com/auth0/express-jwt)
1616
- [express-session](https://github.com/expressjs/session)
17+
- [fast-json-parse](https://github.com/mcollina/fast-json-parse)
1718
- [forge](https://github.com/digitalbazaar/forge)
19+
- [json-parse-better-errors](https://github.com/zkat/json-parse-better-errors)
20+
- [json-parse-safe](https://github.com/joaquimserafim/json-parse-safe)
21+
- [json-safe-parse](https://github.com/bahamas10/node-json-safe-parse)
1822
- [MySQL2](https://github.com/sidorares/node-mysql2)
23+
- [parse-json](https://github.com/sindresorhus/parse-json)
1924
- [q](http://documentup.com/kriskowal/q/)
20-
25+
- [safe-json-parse](https://github.com/Raynos/safe-json-parse)
26+
2127
## New queries
2228

2329
| **Query** | **Tags** | **Purpose** |
@@ -43,3 +49,4 @@
4349
## Changes to QL libraries
4450

4551
* HTTP header names are now always normalized to lower case to reflect the fact that they are case insensitive. In particular, the result of `HeaderDefinition.getAHeaderName`, and the first parameter of `HeaderDefinition.defines`, `ExplicitHeaderDefinition.definesExplicitly` and `RouteHandler.getAResponseHeader` is now always a lower-case string.
52+
* The class `JsonParseCall` has been deprecated. Use `JsonParserCall` instead.

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import semmle.javascript.Functions
2323
import semmle.javascript.HTML
2424
import semmle.javascript.JSDoc
2525
import semmle.javascript.JSON
26+
import semmle.javascript.JsonParsers
2627
import semmle.javascript.JSX
2728
import semmle.javascript.Lines
2829
import semmle.javascript.Locations
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Provides classes for working with JSON parsers.
3+
*/
4+
import javascript
5+
6+
/**
7+
* A call to a JSON parser such as `JSON.parse`.
8+
*/
9+
abstract class JsonParserCall extends DataFlow::CallNode {
10+
/**
11+
* Gets the data flow node holding the input string to be parsed.
12+
*/
13+
abstract DataFlow::Node getInput();
14+
15+
/**
16+
* Gets the data flow node holding the resulting JSON object.
17+
*/
18+
abstract DataFlow::SourceNode getOutput();
19+
}
20+
21+
/**
22+
* A JSON parser that returns its result.
23+
*/
24+
private class PlainJsonParserCall extends JsonParserCall {
25+
PlainJsonParserCall() {
26+
exists (DataFlow::SourceNode callee | this = callee.getACall() |
27+
callee = DataFlow::globalVarRef("JSON").getAPropertyRead("parse") or
28+
callee = DataFlow::moduleImport("parse-json") or
29+
callee = DataFlow::moduleImport("json-parse-better-errors") or
30+
callee = DataFlow::moduleImport("json-safe-parse"))
31+
}
32+
33+
override DataFlow::Node getInput() {
34+
result = getArgument(0)
35+
}
36+
37+
override DataFlow::SourceNode getOutput() {
38+
result = this
39+
}
40+
}
41+
42+
/**
43+
* A JSON parser that returns its result wrapped in a another object.
44+
*/
45+
private class JsonParserCallWithWrapper extends JsonParserCall {
46+
string outputPropName;
47+
48+
JsonParserCallWithWrapper() {
49+
exists (DataFlow::SourceNode callee | this = callee.getACall() |
50+
callee = DataFlow::moduleImport("safe-json-parse/tuple") and outputPropName = "1" or
51+
callee = DataFlow::moduleImport("safe-json-parse/result") and outputPropName = "v" or
52+
callee = DataFlow::moduleImport("fast-json-parse") and outputPropName = "value" or
53+
callee = DataFlow::moduleImport("json-parse-safe") and outputPropName = "value")
54+
}
55+
56+
override DataFlow::Node getInput() {
57+
result = getArgument(0)
58+
}
59+
60+
override DataFlow::SourceNode getOutput() {
61+
result = this.getAPropertyRead(outputPropName)
62+
}
63+
}
64+
65+
/**
66+
* A JSON parser that returns its result through a callback argument.
67+
*/
68+
private class JsonParserCallWithCallback extends JsonParserCall {
69+
JsonParserCallWithCallback() {
70+
this = DataFlow::moduleImport("safe-json-parse/callback").getACall()
71+
}
72+
73+
override DataFlow::Node getInput() {
74+
result = getArgument(0)
75+
}
76+
77+
override DataFlow::SourceNode getOutput() {
78+
result = getCallback(1).getParameter(1)
79+
}
80+
}

javascript/ql/src/semmle/javascript/StandardLibrary.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ class DirectEval extends CallExpr {
4444
}
4545

4646
/**
47+
* DEPRECATED. Use `JsonParserCall` and the data flow API instead.
48+
*
4749
* A call to `JSON.parse`.
4850
*/
51+
deprecated
4952
class JsonParseCall extends MethodCallExpr {
5053
JsonParseCall() {
5154
this = DataFlow::globalVarRef("JSON").getAMemberCall("parse").asExpr()

javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,21 +400,32 @@ module TaintTracking {
400400
}
401401

402402
/**
403-
* A taint propagating data flow edge arising from JSON parsing or unparsing.
403+
* A taint propagating data flow edge arising from JSON unparsing.
404404
*/
405-
private class JsonManipulationTaintStep extends AdditionalTaintStep, DataFlow::MethodCallNode {
406-
JsonManipulationTaintStep() {
407-
exists (string methodName |
408-
methodName = "parse" or methodName = "stringify" |
409-
this = DataFlow::globalVarRef("JSON").getAMemberCall(methodName)
410-
)
405+
private class JsonStringifyTaintStep extends AdditionalTaintStep, DataFlow::MethodCallNode {
406+
JsonStringifyTaintStep() {
407+
this = DataFlow::globalVarRef("JSON").getAMemberCall("stringify")
411408
}
412409

413410
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
414411
pred = getArgument(0) and succ = this
415412
}
416413
}
417414

415+
/**
416+
* A taint propagating data flow edge arising from JSON parsing.
417+
*/
418+
private class JsonParserTaintStep extends AdditionalTaintStep, DataFlow::CallNode {
419+
JsonParserCall call;
420+
421+
JsonParserTaintStep() { this = call }
422+
423+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
424+
pred = call.getInput() and
425+
succ = call.getOutput()
426+
}
427+
}
428+
418429
/**
419430
* Holds if `params` is a `URLSearchParams` object providing access to
420431
* the parameters encoded in `input`.

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

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -55,36 +55,6 @@ module NosqlInjection {
5555
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
5656
}
5757

58-
/**
59-
* A taint tracking configuration for tracking user input that flows
60-
* into a call to `JSON.parse`.
61-
*/
62-
private class RemoteJsonTrackingConfig extends TaintTracking::Configuration {
63-
RemoteJsonTrackingConfig() {
64-
this = "RemoteJsonTrackingConfig"
65-
}
66-
67-
override predicate isSource(DataFlow::Node nd) {
68-
nd instanceof RemoteFlowSource
69-
}
70-
71-
override predicate isSink(DataFlow::Node nd) {
72-
nd.asExpr() = any(JsonParseCall c).getArgument(0)
73-
}
74-
}
75-
76-
/**
77-
* A call to `JSON.parse` where the argument is user-provided.
78-
*/
79-
class RemoteJson extends Source, DataFlow::ValueNode {
80-
RemoteJson() {
81-
exists (DataFlow::Node parsedArg |
82-
parsedArg.asExpr() = astNode.(JsonParseCall).getArgument(0) and
83-
any(RemoteJsonTrackingConfig cfg).hasFlow(_, parsedArg)
84-
)
85-
}
86-
}
87-
8858
/** An expression interpreted as a NoSQL query, viewed as a sink. */
8959
class NosqlQuerySink extends Sink, DataFlow::ValueNode {
9060
override NoSQL::Query astNode;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
| tst.js:11:1:11:28 | checkJS ... input)) | OK |
2+
| tst.js:12:1:12:39 | checkJS ... input)) | OK |
3+
| tst.js:13:1:13:53 | checkJS ... input)) | OK |
4+
| tst.js:14:1:14:44 | checkJS ... input)) | OK |
5+
| tst.js:16:1:16:53 | checkJS ... ut)[1]) | OK |
6+
| tst.js:17:1:17:53 | checkJS ... put).v) | OK |
7+
| tst.js:18:1:18:50 | checkJS ... .value) | OK |
8+
| tst.js:19:1:19:50 | checkJS ... .value) | OK |
9+
| tst.js:21:61:21:77 | checkJSON(result) | OK |
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import javascript
2+
3+
class Assertion extends DataFlow::CallNode {
4+
Assertion() {
5+
getCalleeName() = "checkJSON"
6+
}
7+
8+
string getMessage() {
9+
if not any(JsonParserCall call).getOutput().flowsTo(getArgument(0)) then
10+
result = "Should be JSON parser"
11+
else
12+
result = "OK"
13+
}
14+
}
15+
16+
from Assertion assertion
17+
select assertion, assertion.getMessage()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"private": true,
3+
"dependencies": {
4+
"fast-json-parse": "^1.0.3",
5+
"json-parse-better-errors": "^1.0.2",
6+
"json-parse-safe": "^1.0.5",
7+
"json-safe-parse": "^0.0.2",
8+
"parse-json": "^4.0.0",
9+
"safe-json-parse": "^4.0.0"
10+
}
11+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
let fs = require('fs'); // mark as node.js module
2+
3+
function checkJSON(value) {
4+
if (value !== 'JSON string') {
5+
throw new Error('Not the JSON output: ' + value);
6+
}
7+
}
8+
9+
let input = '"JSON string"';
10+
11+
checkJSON(JSON.parse(input));
12+
checkJSON(require('parse-json')(input));
13+
checkJSON(require('json-parse-better-errors')(input));
14+
checkJSON(require('json-safe-parse')(input));
15+
16+
checkJSON(require('safe-json-parse/tuple')(input)[1]);
17+
checkJSON(require('safe-json-parse/result')(input).v);
18+
checkJSON(require('fast-json-parse')(input).value);
19+
checkJSON(require('json-parse-safe')(input).value);
20+
21+
require('safe-json-parse/callback')(input, (err, result) => checkJSON(result));

0 commit comments

Comments
 (0)