Skip to content

JS: Taint propagation from low-level ArrayBuffer to Strings #19231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions javascript/ql/lib/change-notes/2025-04-07-typed-arrays.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added taint propagation for `Uint8Array`, `ArrayBuffer`, `SharedArrayBuffer` and `TextDecoder.decode()`.
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ private import Sets
private import Strings
private import DynamicImportStep
private import UrlSearchParams
private import TypedArrays
private import Decoders
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
private import javascript
private import semmle.javascript.dataflow.FlowSummary
private import semmle.javascript.dataflow.InferredTypes
private import semmle.javascript.dataflow.internal.DataFlowPrivate as Private
private import FlowSummaryUtil

private class TextDecoderEntryPoint extends API::EntryPoint {
TextDecoderEntryPoint() { this = "global.TextDecoder" }

override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("TextDecoder") }
}

pragma[nomagic]
API::Node textDecoderConstructorRef() { result = any(TextDecoderEntryPoint e).getANode() }

class Decode extends SummarizedCallable {
Decode() { this = "TextDecoder#decode" }

override InstanceCall getACall() {
result = textDecoderConstructorRef().getInstance().getMember("decode").getACall()
}

override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = false and
input = "Argument[0].ArrayElement" and
output = "ReturnValue"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,19 @@ class StringSplitHashOrQuestionMark extends SummarizedCallable {
)
}
}

class StringFromCharCode extends SummarizedCallable {
StringFromCharCode() { this = "String#fromCharCode" }

override DataFlow::CallNode getACall() {
result = DataFlow::globalVarRef("String").getAPropertyRead("fromCharCode").getACall()
}

override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = false and
(
input = "Argument[0..]" and
output = "ReturnValue"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
private import javascript
private import semmle.javascript.dataflow.FlowSummary
private import semmle.javascript.dataflow.InferredTypes
private import semmle.javascript.dataflow.internal.DataFlowPrivate as Private
private import FlowSummaryUtil

private class TypedArrayEntryPoint extends API::EntryPoint {
TypedArrayEntryPoint() { this = "global.Uint8Array" }

override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("Uint8Array") }
}

pragma[nomagic]
API::Node typedArrayConstructorRef() { result = any(TypedArrayEntryPoint e).getANode() }

class TypedArrayConstructorSummary extends SummarizedCallable {
TypedArrayConstructorSummary() { this = "TypedArray constructor" }

override DataFlow::InvokeNode getACall() {
result = typedArrayConstructorRef().getAnInstantiation()
}

override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0].ArrayElement" and
output = "ReturnValue.ArrayElement"
}
}

class BufferTypedArray extends DataFlow::AdditionalFlowStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::PropRead p |
p = typedArrayConstructorRef().getInstance().getMember("buffer").asSource() and
pred = p.getBase() and
succ = p
)
}
}

class TypedArraySet extends SummarizedCallable {
TypedArraySet() { this = "TypedArray#set" }

override InstanceCall getACall() {
result = typedArrayConstructorRef().getInstance().getMember("set").getACall()
}

override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0].ArrayElement" and
output = "Argument[this].ArrayElement"
}
}

class TypedArraySubarray extends SummarizedCallable {
TypedArraySubarray() { this = "TypedArray#subarray" }

override InstanceCall getACall() { result.getMethodName() = "subarray" }

override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[this].ArrayElement" and
output = "ReturnValue.ArrayElement"
}
}

private class ArrayBufferEntryPoint extends API::EntryPoint {
ArrayBufferEntryPoint() { this = ["global.ArrayBuffer", "global.SharedArrayBuffer"] }

override DataFlow::SourceNode getASource() {
result = DataFlow::globalVarRef(["ArrayBuffer", "SharedArrayBuffer"])
}
}

pragma[nomagic]
API::Node arrayBufferConstructorRef() { result = any(ArrayBufferEntryPoint a).getANode() }

class TransferLike extends SummarizedCallable {
TransferLike() { this = "ArrayBuffer#transfer" }

override InstanceCall getACall() {
result.getMethodName() = ["transfer", "transferToFixedLength"]
}

override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[this].ArrayElement" and
output = "ReturnValue.ArrayElement"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,23 @@ legacyDataFlowDifference
| spread.js:4:15:4:22 | source() | spread.js:18:8:18:8 | y | only flow with NEW data flow library |
| spread.js:4:15:4:22 | source() | spread.js:24:8:24:8 | y | only flow with NEW data flow library |
| tst.js:2:13:2:20 | source() | tst.js:17:10:17:10 | a | only flow with OLD data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:5:10:5:10 | y | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:7:10:7:17 | y.buffer | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:11:10:11:12 | arr | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:15:10:15:10 | z | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:18:10:18:12 | sub | only flow with NEW data flow library |
| 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 |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:48:10:48:12 | str | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:52:10:52:13 | str2 | only flow with NEW data flow library |
| 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 |
consistencyIssue
| nested-props.js:20 | expected an alert, but found none | NOT OK - but not found | Consistency |
| stringification-read-steps.js:17 | expected an alert, but found none | NOT OK | Consistency |
| stringification-read-steps.js:25 | expected an alert, but found none | NOT OK | Consistency |
| typed-arrays.js:23 | expected an alert, but found none | NOT OK -- Should be flagged but it is not. | Consistency |
| typed-arrays.js:28 | expected an alert, but found none | NOT OK -- Should be flagged but it is not. | Consistency |
| typed-arrays.js:32 | expected an alert, but found none | NOT OK -- Should be flagged but it is not. | Consistency |
| typed-arrays.js:36 | expected an alert, but found none | NOT OK -- Should be flagged but it is not. | Consistency |
flow
| access-path-sanitizer.js:2:18:2:25 | source() | access-path-sanitizer.js:4:8:4:12 | obj.x |
| addexpr.js:4:10:4:17 | source() | addexpr.js:7:8:7:8 | x |
Expand Down Expand Up @@ -325,6 +337,14 @@ flow
| tst.js:87:22:87:29 | source() | tst.js:90:14:90:25 | taintedValue |
| tst.js:93:22:93:29 | source() | tst.js:96:14:96:25 | taintedValue |
| tst.js:93:22:93:29 | source() | tst.js:97:14:97:26 | map.get(true) |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:5:10:5:10 | y |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:7:10:7:17 | y.buffer |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:11:10:11:12 | arr |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:15:10:15:10 | z |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:18:10:18:12 | sub |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:42:10:42:30 | typedAr ... ring(y) |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:48:10:48:12 | str |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:52:10:52:13 | str2 |
| use-use-after-implicit-read.js:7:17:7:24 | source() | use-use-after-implicit-read.js:8:10:8:17 | captured |
| use-use-after-implicit-read.js:7:17:7:24 | source() | use-use-after-implicit-read.js:15:10:15:10 | x |
| xml.js:5:18:5:25 | source() | xml.js:8:14:8:17 | text |
Expand Down
53 changes: 53 additions & 0 deletions javascript/ql/test/library-tests/TaintTracking/typed-arrays.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
function test() {
let x = source();

let y = new Uint8Array(x);
sink(y); // NOT OK

sink(y.buffer); // NOT OK
sink(y.length);

var arr = new Uint8Array(y.buffer, y.byteOffset, y.byteLength);
sink(arr); // NOT OK

const z = new Uint8Array([1, 2, 3]);
z.set(y, 3);
sink(z); // NOT OK

const sub = y.subarray(1, 3)
sink(sub); // NOT OK

const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view.set(x, 3);
sink(buffer); // NOT OK -- Should be flagged but it is not.

const sharedBuffer = new SharedArrayBuffer(8);
const view1 = new Uint8Array(sharedBuffer);
view1.set(x, 3);
sink(sharedBuffer); // NOT OK -- Should be flagged but it is not.

const transfered = buffer.transfer();
const transferedView = new Uint8Array(transfered);
sink(transferedView); // NOT OK -- Should be flagged but it is not.

const transfered2 = buffer.transferToFixedLength();
const transferedView2 = new Uint8Array(transfered2);
sink(transferedView2); // NOT OK -- Should be flagged but it is not.

var typedArrayToString = (function () {
return function (a) { return String.fromCharCode.apply(null, a); };
})();

sink(typedArrayToString(y)); // NOT OK

let str = '';
for (let i = 0; i < y.length; i++)
str += String.fromCharCode(y[i]);

sink(str); // NOT OK

const decoder = new TextDecoder('utf-8');
const str2 = decoder.decode(y);
sink(str2); // NOT OK
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,12 @@ edges
| pako.js:18:48:18:66 | zipFile.data.buffer | pako.js:18:33:18:67 | new Uin ... buffer) | provenance | Config |
| pako.js:28:19:28:25 | zipFile | pako.js:29:36:29:42 | zipFile | provenance | |
| pako.js:29:11:29:62 | myArray | pako.js:32:31:32:37 | myArray | provenance | |
| pako.js:29:11:29:62 | myArray [ArrayElement] | pako.js:32:31:32:37 | myArray | provenance | |
| pako.js:29:21:29:55 | new Uin ... buffer) | pako.js:29:11:29:62 | myArray | provenance | |
| pako.js:29:21:29:55 | new Uin ... buffer) [ArrayElement] | pako.js:29:11:29:62 | myArray [ArrayElement] | provenance | |
| pako.js:29:36:29:42 | zipFile | pako.js:29:36:29:54 | zipFile.data.buffer | provenance | |
| pako.js:29:36:29:54 | zipFile.data.buffer | pako.js:29:21:29:55 | new Uin ... buffer) | provenance | Config |
| pako.js:29:36:29:54 | zipFile.data.buffer | pako.js:29:21:29:55 | new Uin ... buffer) [ArrayElement] | provenance | |
| unbzip2.js:12:5:12:43 | fs.crea ... lePath) | unbzip2.js:12:50:12:54 | bz2() | provenance | Config |
| unbzip2.js:12:25:12:42 | req.query.FilePath | unbzip2.js:12:5:12:43 | fs.crea ... lePath) | provenance | Config |
| unzipper.js:13:26:13:62 | Readabl ... e.data) | unzipper.js:16:23:16:63 | unzippe ... ath' }) | provenance | Config |
Expand Down Expand Up @@ -183,7 +186,9 @@ nodes
| pako.js:21:31:21:37 | myArray | semmle.label | myArray |
| pako.js:28:19:28:25 | zipFile | semmle.label | zipFile |
| pako.js:29:11:29:62 | myArray | semmle.label | myArray |
| pako.js:29:11:29:62 | myArray [ArrayElement] | semmle.label | myArray [ArrayElement] |
| pako.js:29:21:29:55 | new Uin ... buffer) | semmle.label | new Uin ... buffer) |
| pako.js:29:21:29:55 | new Uin ... buffer) [ArrayElement] | semmle.label | new Uin ... buffer) [ArrayElement] |
| pako.js:29:36:29:42 | zipFile | semmle.label | zipFile |
| pako.js:29:36:29:54 | zipFile.data.buffer | semmle.label | zipFile.data.buffer |
| pako.js:32:31:32:37 | myArray | semmle.label | myArray |
Expand Down