Skip to content

Commit e45268c

Browse files
committed
improve and fix bugs and add Form Flow Sources test files
1 parent eef8137 commit e45268c

File tree

8 files changed

+395
-7
lines changed

8 files changed

+395
-7
lines changed

javascript/ql/lib/semmle/javascript/frameworks/FormParsers.qll

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module BusBoy {
2020
// Files
2121
busboyOnEvent.getParameter(0).asSink().mayHaveStringValue("file") and
2222
// second param of 'file' event is a Readable stream
23-
this = readableStreamDataNode(busboyOnEvent.getParameter(1).getParameter(1)).asSource()
23+
this = readableStreamDataNode(busboyOnEvent.getParameter(1).getParameter(1))
2424
or
2525
// Fields
2626
busboyOnEvent.getParameter(0).asSink().mayHaveStringValue(["file", "field"]) and
@@ -53,6 +53,15 @@ module BusBoy {
5353
}
5454
}
5555

56+
predicate step(DataFlow::Node pred, DataFlow::Node succ) {
57+
exists(API::Node busboyOnEvent |
58+
busboyOnEvent = API::moduleImport("busboy").getReturn().getMember("on")
59+
|
60+
busboyOnEvent.getParameter(0).asSink().mayHaveStringValue("file") and
61+
customStreamPipeAdditionalTaintStep(busboyOnEvent.getParameter(1).getParameter(1), pred, succ)
62+
)
63+
}
64+
5665
/**
5766
* A module for modeling [formidable](https://www.npmjs.com/package/formidable) package
5867
*/
@@ -112,7 +121,7 @@ module Multiparty {
112121
this = on.getParameter(1).getParameter([0, 1]).asSource()
113122
or
114123
on.getParameter(0).asSink().mayHaveStringValue("part") and
115-
this = readableStreamDataNode(on.getParameter(1).getParameter(0)).asSource()
124+
this = readableStreamDataNode(on.getParameter(1).getParameter(0))
116125
)
117126
)
118127
)
@@ -150,7 +159,7 @@ module Dicer {
150159
exists(API::Node dicer | dicer = API::moduleImport("dicer").getInstance() |
151160
exists(API::Node on | on = dicer.getMember("on") |
152161
on.getParameter(0).asSink().mayHaveStringValue("part") and
153-
this = readableStreamDataNode(on.getParameter(1).getParameter(0)).asSource()
162+
this = readableStreamDataNode(on.getParameter(1).getParameter(0))
154163
or
155164
exists(API::Node onPart | onPart = on.getParameter(1).getParameter(0).getMember("on") |
156165
onPart.getParameter(0).asSink().mayHaveStringValue("header") and
@@ -177,3 +186,14 @@ module Dicer {
177186
}
178187
}
179188
}
189+
190+
/**
191+
* An Additional taint step like `for (succ in pred)`
192+
*/
193+
private class AdditionalTaintStepForIn extends TaintTracking::SharedTaintStep {
194+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
195+
exists(ForInStmt fis, Variable v | v = fis.getAnIterationVariable() |
196+
succ.asExpr() = v.getAnAccess() and pred.asExpr() = fis.getIterationDomain()
197+
)
198+
}
199+
}

javascript/ql/lib/semmle/javascript/frameworks/ReadableStream.qll

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,20 @@ API::Node nodeJsStream() {
117117
* Gets a Readable Stream method(not a return value of the method)
118118
* and returns all nodes responsible for a data read access
119119
*/
120-
API::Node readableStreamDataNode(API::Node stream) {
120+
DataFlow::Node readableStreamDataNode(API::Node stream) {
121+
result = stream.asSource()
122+
or
121123
// 'data' event
122124
exists(API::CallNode onEvent | onEvent = stream.getMember("on").getACall() |
123-
result = onEvent.getParameter(1).getParameter(0) and
125+
result = onEvent.getParameter(1).getParameter(0).asSource() and
124126
onEvent.getParameter(0).asSink().mayHaveStringValue("data")
125127
)
126128
or
127129
// 'Readable' event
128130
exists(API::CallNode onEvent | onEvent = stream.getMember("on").getACall() |
129131
(
130-
result = onEvent.getParameter(1).getReceiver().getMember("read").getReturn() or
131-
result = stream.getMember("read").getReturn()
132+
result = onEvent.getParameter(1).getReceiver().getMember("read").getReturn().asSource() or
133+
result = stream.getMember("read").getReturn().asSource()
132134
) and
133135
onEvent.getParameter(0).asSink().mayHaveStringValue("readable")
134136
)

javascript/ql/test/library-tests/frameworks/FormParsers/RemoteFlowSource.expected

Lines changed: 234 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @name Remote Form Flow Sources
3+
* @description Using remote user controlled sources from Forms
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 5
7+
* @precision high
8+
* @id js/remote-flow-source
9+
* @tags correctness
10+
* security
11+
*/
12+
13+
import javascript
14+
import DataFlow::PathGraph
15+
16+
/**
17+
* A taint-tracking configuration for test
18+
*/
19+
class Configuration extends TaintTracking::Configuration {
20+
Configuration() { this = "RemoteFlowSourcesOUserForm" }
21+
22+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
23+
24+
override predicate isSink(DataFlow::Node sink) {
25+
sink = API::moduleImport("sink").getAParameter().asSink() or
26+
sink = API::moduleImport("sink").getReturn().asSource()
27+
}
28+
}
29+
30+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
31+
where cfg.hasFlowPath(source, sink)
32+
select sink.getNode(), source, sink, "This entity depends on a $@.", source.getNode(),
33+
"user-provided value"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const http = require('http');
2+
const zlib = require('node:zlib');
3+
const busboy = require('busboy');
4+
const sink = require('sink');
5+
6+
http.createServer((req, res) => {
7+
if (req.method === 'POST') {
8+
const bb = busboy({ headers: req.headers });
9+
bb.on('file', (name, file, info) => {
10+
const { filename, encoding, mimeType } = info;
11+
const z = zlib.createGzip();
12+
sink(filename, encoding, mimeType) // sink
13+
file.pipe(z).pipe(sink())
14+
15+
file.on('data', (data) => {
16+
sink(data)
17+
})
18+
19+
file.on('readable', function () {
20+
// There is some data to read now.
21+
let data;
22+
while ((data = this.read()) !== null) {
23+
sink(data)
24+
}
25+
});
26+
});
27+
bb.on('field', (name, val, info) => {
28+
sink(name, val, info)
29+
});
30+
}
31+
}).listen(8000, () => {
32+
console.log('Listening for requests');
33+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { inspect } = require('util');
2+
const http = require('http');
3+
const Dicer = require('dicer');
4+
const sink = require('sink');
5+
6+
const PORT = 8080;
7+
8+
http.createServer((req, res) => {
9+
let m;
10+
const dicer = new Dicer({ boundary: m[1] || m[2] });
11+
12+
dicer.on('part', (part) => {
13+
part.pipe(sink())
14+
part.on('header', (header) => {
15+
for (h in header) {
16+
sink(h)
17+
}
18+
});
19+
part.on('data', (data) => {
20+
sink(data)
21+
});
22+
});
23+
}).listen(PORT, () => {
24+
console.log(`Listening for requests on port ${PORT}`);
25+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import http from 'node:http';
2+
import formidable from 'formidable';
3+
const sink = require('sink');
4+
5+
const server = http.createServer(async (req, res) => {
6+
const form = formidable({});
7+
const [fields, files] = await form.parse(req);
8+
sink(fields, files)
9+
form.on('fileBegin', (formname, file) => {
10+
sink(formname, file)
11+
});
12+
form.on('file', (formname, file) => {
13+
sink(formname, file)
14+
});
15+
form.on('field', (fieldName, fieldValue) => {
16+
sink(fieldName, fieldValue)
17+
});
18+
});
19+
20+
server.listen(8080, () => {
21+
console.log('Server listening on http://localhost:8080/ ...');
22+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
var multiparty = require('multiparty');
2+
var http = require('http');
3+
var util = require('util');
4+
const sink = require('sink');
5+
6+
http.createServer(function (req, res) {
7+
var form = new multiparty.Form();
8+
form.on('part', (part) => {
9+
sink(part)
10+
part.pipe(sink())
11+
});
12+
13+
var form2 = new multiparty.Form();
14+
form2.parse(req, function (err, fields, files) {
15+
sink(fields, files)
16+
});
17+
form2.parse(req);
18+
19+
}).listen(8080);

0 commit comments

Comments
 (0)