diff --git a/javascript/ql/lib/change-notes/2025-04-07-typed-arrays.md b/javascript/ql/lib/change-notes/2025-04-07-typed-arrays.md new file mode 100644 index 000000000000..f09e6831743b --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-04-07-typed-arrays.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added taint propagation for `Uint8Array`, `ArrayBuffer`, `SharedArrayBuffer` and `TextDecoder.decode()`. diff --git a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/AllFlowSummaries.qll b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/AllFlowSummaries.qll index 940180d80cb4..dfbe9ef7da31 100644 --- a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/AllFlowSummaries.qll +++ b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/AllFlowSummaries.qll @@ -12,3 +12,5 @@ private import Sets private import Strings private import DynamicImportStep private import UrlSearchParams +private import TypedArrays +private import Decoders diff --git a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Decoders.qll b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Decoders.qll new file mode 100644 index 000000000000..2866c8926087 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Decoders.qll @@ -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" + } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Strings.qll b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Strings.qll index 154668cde080..d18e21819653 100644 --- a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Strings.qll +++ b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Strings.qll @@ -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" + ) + } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/TypedArrays.qll b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/TypedArrays.qll new file mode 100644 index 000000000000..19a28036db49 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/TypedArrays.qll @@ -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" + } +} diff --git a/javascript/ql/test/library-tests/TaintTracking/BasicTaintTracking.expected b/javascript/ql/test/library-tests/TaintTracking/BasicTaintTracking.expected index 4a0575eb73e1..0083e55e642e 100644 --- a/javascript/ql/test/library-tests/TaintTracking/BasicTaintTracking.expected +++ b/javascript/ql/test/library-tests/TaintTracking/BasicTaintTracking.expected @@ -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 | @@ -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 | diff --git a/javascript/ql/test/library-tests/TaintTracking/typed-arrays.js b/javascript/ql/test/library-tests/TaintTracking/typed-arrays.js new file mode 100644 index 000000000000..0118c2ae6904 --- /dev/null +++ b/javascript/ql/test/library-tests/TaintTracking/typed-arrays.js @@ -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 +} diff --git a/javascript/ql/test/query-tests/Security/CWE-522-DecompressionBombs/DecompressionBombs.expected b/javascript/ql/test/query-tests/Security/CWE-522-DecompressionBombs/DecompressionBombs.expected index 56acd5390128..11c63c257e83 100644 --- a/javascript/ql/test/query-tests/Security/CWE-522-DecompressionBombs/DecompressionBombs.expected +++ b/javascript/ql/test/query-tests/Security/CWE-522-DecompressionBombs/DecompressionBombs.expected @@ -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 | @@ -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 |