Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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,29 @@
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 DecodeLike extends SummarizedCallable {
DecodeLike() { this = "TextDecoder#decode" }

override InstanceCall getACall() {
result =
textDecoderConstructorRef().getAnInstantiation().getReturn().getMember("decode").getACall()
Comment thread
Napalys marked this conversation as resolved.
Outdated
}

override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = true and
Comment thread
Napalys marked this conversation as resolved.
Outdated
input = "Argument[0]" and
output = "ReturnValue"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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 SetLike extends SummarizedCallable {
Comment thread
Napalys marked this conversation as resolved.
Outdated
SetLike() { this = "TypedArray#set" }

override InstanceCall getACall() {
result = typedArrayConstructorRef().getAnInstantiation().getReturn().getMember("set").getACall()
Comment thread
Napalys marked this conversation as resolved.
Outdated
}

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

class SubArrayLike extends SummarizedCallable {
Comment thread
Napalys marked this conversation as resolved.
Outdated
SubArrayLike() { this = "TypedArray#subarray" }

override InstanceCall getACall() {
result =
typedArrayConstructorRef().getAnInstantiation().getReturn().getMember("subarray").getACall()
Comment thread
Napalys marked this conversation as resolved.
Outdated
}

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 ArrayBufferConstructorSummary extends SummarizedCallable {
ArrayBufferConstructorSummary() { this = "ArrayBuffer constructor" }

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

override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0].ArrayElement" and
output = "ReturnValue.ArrayElement"
Comment thread
Napalys marked this conversation as resolved.
Outdated
}
}

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

override InstanceCall getACall() {
result =
arrayBufferConstructorRef()
.getAnInstantiation()
.getReturn()
.getMember(["transfer", "transferToFixedLength"])
.getACall()
Comment thread
Napalys marked this conversation as resolved.
Outdated
}

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:22:10:22:13 | view | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:26:10:26:14 | view1 | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:30:10:30:23 | transferedView | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:34:10:34:24 | transferedView2 | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:46:10:46:12 | str | only flow with NEW data flow library |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:50:10:50: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:40 | 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,17 @@ 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:22:10:22:13 | view |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:26:10:26:14 | view1 |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:30:10:30:23 | transferedView |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:34:10:34:24 | transferedView2 |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:46:10:46:12 | str |
| typed-arrays.js:2:13:2:20 | source() | typed-arrays.js:50:10:50: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
51 changes: 51 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,51 @@
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(x);
Comment thread
Napalys marked this conversation as resolved.
Outdated
const view = new Uint8Array(buffer);
sink(view); // NOT OK

const sharedBuffer = new SharedArrayBuffer(x);
const view1 = new Uint8Array(sharedBuffer);
sink(view1); // NOT OK

const transfered = buffer.transfer();
const transferedView = new Uint8Array(transfered);
sink(transferedView); // NOT OK

const transfered2 = buffer.transferToFixedLength();
const transferedView2 = new Uint8Array(transfered2);
sink(transferedView2); // NOT OK

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

sink(typedArrayToString(y)); // NOT OK -- Should be flagged but it is not.
Comment thread
Napalys marked this conversation as resolved.
Outdated

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