Skip to content

Commit 3d7c020

Browse files
authored
Merge pull request #19231 from Napalys/js/typed_array
JS: Taint propagation from low-level `ArrayBuffer` to `Strings`
2 parents d17d29a + d0dcf89 commit 3d7c020

File tree

8 files changed

+217
-0
lines changed

8 files changed

+217
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added taint propagation for `Uint8Array`, `ArrayBuffer`, `SharedArrayBuffer` and `TextDecoder.decode()`.

javascript/ql/lib/semmle/javascript/internal/flow_summaries/AllFlowSummaries.qll

+2
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ private import Sets
1212
private import Strings
1313
private import DynamicImportStep
1414
private import UrlSearchParams
15+
private import TypedArrays
16+
private import Decoders
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
private import javascript
2+
private import semmle.javascript.dataflow.FlowSummary
3+
private import semmle.javascript.dataflow.InferredTypes
4+
private import semmle.javascript.dataflow.internal.DataFlowPrivate as Private
5+
private import FlowSummaryUtil
6+
7+
private class TextDecoderEntryPoint extends API::EntryPoint {
8+
TextDecoderEntryPoint() { this = "global.TextDecoder" }
9+
10+
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("TextDecoder") }
11+
}
12+
13+
pragma[nomagic]
14+
API::Node textDecoderConstructorRef() { result = any(TextDecoderEntryPoint e).getANode() }
15+
16+
class Decode extends SummarizedCallable {
17+
Decode() { this = "TextDecoder#decode" }
18+
19+
override InstanceCall getACall() {
20+
result = textDecoderConstructorRef().getInstance().getMember("decode").getACall()
21+
}
22+
23+
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
24+
preservesValue = false and
25+
input = "Argument[0].ArrayElement" and
26+
output = "ReturnValue"
27+
}
28+
}

javascript/ql/lib/semmle/javascript/internal/flow_summaries/Strings.qll

+16
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,19 @@ class StringSplitHashOrQuestionMark extends SummarizedCallable {
9999
)
100100
}
101101
}
102+
103+
class StringFromCharCode extends SummarizedCallable {
104+
StringFromCharCode() { this = "String#fromCharCode" }
105+
106+
override DataFlow::CallNode getACall() {
107+
result = DataFlow::globalVarRef("String").getAPropertyRead("fromCharCode").getACall()
108+
}
109+
110+
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
111+
preservesValue = false and
112+
(
113+
input = "Argument[0..]" and
114+
output = "ReturnValue"
115+
)
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
private import javascript
2+
private import semmle.javascript.dataflow.FlowSummary
3+
private import semmle.javascript.dataflow.InferredTypes
4+
private import semmle.javascript.dataflow.internal.DataFlowPrivate as Private
5+
private import FlowSummaryUtil
6+
7+
private class TypedArrayEntryPoint extends API::EntryPoint {
8+
TypedArrayEntryPoint() { this = "global.Uint8Array" }
9+
10+
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("Uint8Array") }
11+
}
12+
13+
pragma[nomagic]
14+
API::Node typedArrayConstructorRef() { result = any(TypedArrayEntryPoint e).getANode() }
15+
16+
class TypedArrayConstructorSummary extends SummarizedCallable {
17+
TypedArrayConstructorSummary() { this = "TypedArray constructor" }
18+
19+
override DataFlow::InvokeNode getACall() {
20+
result = typedArrayConstructorRef().getAnInstantiation()
21+
}
22+
23+
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
24+
preservesValue = true and
25+
input = "Argument[0].ArrayElement" and
26+
output = "ReturnValue.ArrayElement"
27+
}
28+
}
29+
30+
class BufferTypedArray extends DataFlow::AdditionalFlowStep {
31+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
32+
exists(DataFlow::PropRead p |
33+
p = typedArrayConstructorRef().getInstance().getMember("buffer").asSource() and
34+
pred = p.getBase() and
35+
succ = p
36+
)
37+
}
38+
}
39+
40+
class TypedArraySet extends SummarizedCallable {
41+
TypedArraySet() { this = "TypedArray#set" }
42+
43+
override InstanceCall getACall() {
44+
result = typedArrayConstructorRef().getInstance().getMember("set").getACall()
45+
}
46+
47+
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
48+
preservesValue = true and
49+
input = "Argument[0].ArrayElement" and
50+
output = "Argument[this].ArrayElement"
51+
}
52+
}
53+
54+
class TypedArraySubarray extends SummarizedCallable {
55+
TypedArraySubarray() { this = "TypedArray#subarray" }
56+
57+
override InstanceCall getACall() { result.getMethodName() = "subarray" }
58+
59+
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
60+
preservesValue = true and
61+
input = "Argument[this].ArrayElement" and
62+
output = "ReturnValue.ArrayElement"
63+
}
64+
}
65+
66+
private class ArrayBufferEntryPoint extends API::EntryPoint {
67+
ArrayBufferEntryPoint() { this = ["global.ArrayBuffer", "global.SharedArrayBuffer"] }
68+
69+
override DataFlow::SourceNode getASource() {
70+
result = DataFlow::globalVarRef(["ArrayBuffer", "SharedArrayBuffer"])
71+
}
72+
}
73+
74+
pragma[nomagic]
75+
API::Node arrayBufferConstructorRef() { result = any(ArrayBufferEntryPoint a).getANode() }
76+
77+
class TransferLike extends SummarizedCallable {
78+
TransferLike() { this = "ArrayBuffer#transfer" }
79+
80+
override InstanceCall getACall() {
81+
result.getMethodName() = ["transfer", "transferToFixedLength"]
82+
}
83+
84+
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
85+
preservesValue = true and
86+
input = "Argument[this].ArrayElement" and
87+
output = "ReturnValue.ArrayElement"
88+
}
89+
}

javascript/ql/test/library-tests/TaintTracking/BasicTaintTracking.expected

+20
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,23 @@ legacyDataFlowDifference
3535
| spread.js:4:15:4:22 | source() | spread.js:18:8:18:8 | y | only flow with NEW data flow library |
3636
| spread.js:4:15:4:22 | source() | spread.js:24:8:24:8 | y | only flow with NEW data flow library |
3737
| tst.js:2:13:2:20 | source() | tst.js:17:10:17:10 | a | only flow with OLD data flow library |
38+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:5:10:5:10 | y | only flow with NEW data flow library |
39+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:7:10:7:17 | y.buffer | only flow with NEW data flow library |
40+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:11:10:11:12 | arr | only flow with NEW data flow library |
41+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:15:10:15:10 | z | only flow with NEW data flow library |
42+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:18:10:18:12 | sub | only flow with NEW data flow library |
43+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:42:10:42:30 | typedAr ... ring(y) | only flow with NEW data flow library |
44+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:48:10:48:12 | str | only flow with NEW data flow library |
45+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:52:10:52:13 | str2 | only flow with NEW data flow library |
3846
| use-use-after-implicit-read.js:7:17:7:24 | source() | use-use-after-implicit-read.js:15:10:15:10 | x | only flow with NEW data flow library |
3947
consistencyIssue
4048
| nested-props.js:20 | expected an alert, but found none | NOT OK - but not found | Consistency |
4149
| stringification-read-steps.js:17 | expected an alert, but found none | NOT OK | Consistency |
4250
| stringification-read-steps.js:25 | expected an alert, but found none | NOT OK | Consistency |
51+
| typed-arrays.js:23 | expected an alert, but found none | NOT OK -- Should be flagged but it is not. | Consistency |
52+
| typed-arrays.js:28 | expected an alert, but found none | NOT OK -- Should be flagged but it is not. | Consistency |
53+
| typed-arrays.js:32 | expected an alert, but found none | NOT OK -- Should be flagged but it is not. | Consistency |
54+
| typed-arrays.js:36 | expected an alert, but found none | NOT OK -- Should be flagged but it is not. | Consistency |
4355
flow
4456
| access-path-sanitizer.js:2:18:2:25 | source() | access-path-sanitizer.js:4:8:4:12 | obj.x |
4557
| addexpr.js:4:10:4:17 | source() | addexpr.js:7:8:7:8 | x |
@@ -325,6 +337,14 @@ flow
325337
| tst.js:87:22:87:29 | source() | tst.js:90:14:90:25 | taintedValue |
326338
| tst.js:93:22:93:29 | source() | tst.js:96:14:96:25 | taintedValue |
327339
| tst.js:93:22:93:29 | source() | tst.js:97:14:97:26 | map.get(true) |
340+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:5:10:5:10 | y |
341+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:7:10:7:17 | y.buffer |
342+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:11:10:11:12 | arr |
343+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:15:10:15:10 | z |
344+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:18:10:18:12 | sub |
345+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:42:10:42:30 | typedAr ... ring(y) |
346+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:48:10:48:12 | str |
347+
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:52:10:52:13 | str2 |
328348
| use-use-after-implicit-read.js:7:17:7:24 | source() | use-use-after-implicit-read.js:8:10:8:17 | captured |
329349
| use-use-after-implicit-read.js:7:17:7:24 | source() | use-use-after-implicit-read.js:15:10:15:10 | x |
330350
| xml.js:5:18:5:25 | source() | xml.js:8:14:8:17 | text |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
function test() {
2+
let x = source();
3+
4+
let y = new Uint8Array(x);
5+
sink(y); // NOT OK
6+
7+
sink(y.buffer); // NOT OK
8+
sink(y.length);
9+
10+
var arr = new Uint8Array(y.buffer, y.byteOffset, y.byteLength);
11+
sink(arr); // NOT OK
12+
13+
const z = new Uint8Array([1, 2, 3]);
14+
z.set(y, 3);
15+
sink(z); // NOT OK
16+
17+
const sub = y.subarray(1, 3)
18+
sink(sub); // NOT OK
19+
20+
const buffer = new ArrayBuffer(8);
21+
const view = new Uint8Array(buffer);
22+
view.set(x, 3);
23+
sink(buffer); // NOT OK -- Should be flagged but it is not.
24+
25+
const sharedBuffer = new SharedArrayBuffer(8);
26+
const view1 = new Uint8Array(sharedBuffer);
27+
view1.set(x, 3);
28+
sink(sharedBuffer); // NOT OK -- Should be flagged but it is not.
29+
30+
const transfered = buffer.transfer();
31+
const transferedView = new Uint8Array(transfered);
32+
sink(transferedView); // NOT OK -- Should be flagged but it is not.
33+
34+
const transfered2 = buffer.transferToFixedLength();
35+
const transferedView2 = new Uint8Array(transfered2);
36+
sink(transferedView2); // NOT OK -- Should be flagged but it is not.
37+
38+
var typedArrayToString = (function () {
39+
return function (a) { return String.fromCharCode.apply(null, a); };
40+
})();
41+
42+
sink(typedArrayToString(y)); // NOT OK
43+
44+
let str = '';
45+
for (let i = 0; i < y.length; i++)
46+
str += String.fromCharCode(y[i]);
47+
48+
sink(str); // NOT OK
49+
50+
const decoder = new TextDecoder('utf-8');
51+
const str2 = decoder.decode(y);
52+
sink(str2); // NOT OK
53+
}

javascript/ql/test/query-tests/Security/CWE-522-DecompressionBombs/DecompressionBombs.expected

+5
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,12 @@ edges
8181
| pako.js:18:48:18:66 | zipFile.data.buffer | pako.js:18:33:18:67 | new Uin ... buffer) | provenance | Config |
8282
| pako.js:28:19:28:25 | zipFile | pako.js:29:36:29:42 | zipFile | provenance | |
8383
| pako.js:29:11:29:62 | myArray | pako.js:32:31:32:37 | myArray | provenance | |
84+
| pako.js:29:11:29:62 | myArray [ArrayElement] | pako.js:32:31:32:37 | myArray | provenance | |
8485
| pako.js:29:21:29:55 | new Uin ... buffer) | pako.js:29:11:29:62 | myArray | provenance | |
86+
| pako.js:29:21:29:55 | new Uin ... buffer) [ArrayElement] | pako.js:29:11:29:62 | myArray [ArrayElement] | provenance | |
8587
| pako.js:29:36:29:42 | zipFile | pako.js:29:36:29:54 | zipFile.data.buffer | provenance | |
8688
| pako.js:29:36:29:54 | zipFile.data.buffer | pako.js:29:21:29:55 | new Uin ... buffer) | provenance | Config |
89+
| pako.js:29:36:29:54 | zipFile.data.buffer | pako.js:29:21:29:55 | new Uin ... buffer) [ArrayElement] | provenance | |
8790
| unbzip2.js:12:5:12:43 | fs.crea ... lePath) | unbzip2.js:12:50:12:54 | bz2() | provenance | Config |
8891
| unbzip2.js:12:25:12:42 | req.query.FilePath | unbzip2.js:12:5:12:43 | fs.crea ... lePath) | provenance | Config |
8992
| unzipper.js:13:26:13:62 | Readabl ... e.data) | unzipper.js:16:23:16:63 | unzippe ... ath' }) | provenance | Config |
@@ -183,7 +186,9 @@ nodes
183186
| pako.js:21:31:21:37 | myArray | semmle.label | myArray |
184187
| pako.js:28:19:28:25 | zipFile | semmle.label | zipFile |
185188
| pako.js:29:11:29:62 | myArray | semmle.label | myArray |
189+
| pako.js:29:11:29:62 | myArray [ArrayElement] | semmle.label | myArray [ArrayElement] |
186190
| pako.js:29:21:29:55 | new Uin ... buffer) | semmle.label | new Uin ... buffer) |
191+
| pako.js:29:21:29:55 | new Uin ... buffer) [ArrayElement] | semmle.label | new Uin ... buffer) [ArrayElement] |
187192
| pako.js:29:36:29:42 | zipFile | semmle.label | zipFile |
188193
| pako.js:29:36:29:54 | zipFile.data.buffer | semmle.label | zipFile.data.buffer |
189194
| pako.js:32:31:32:37 | myArray | semmle.label | myArray |

0 commit comments

Comments
 (0)