diff --git a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Arrays.qll b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Arrays.qll index b56968377816..6754d3db3071 100644 --- a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Arrays.qll +++ b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Arrays.qll @@ -544,6 +544,25 @@ class ToSpliced extends SummarizedCallable { } } +class With extends SummarizedCallable { + With() { this = "Array#with" } + + override InstanceCall getACallSimple() { result.getMethodName() = "with" } + + override predicate propagatesFlow(string input, string output, boolean preservesValue) { + preservesValue = true and + ( + // Copy all elements from the original array to the new array + input = "Argument[this].WithArrayElement" and + output = "ReturnValue" + or + // Replace the value at the specified index + input = "Argument[1]" and + output = "ReturnValue.ArrayElement" + ) + } +} + class ArrayCoercionPackage extends FunctionalPackageSummary { ArrayCoercionPackage() { this = "ArrayCoercionPackage" } diff --git a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/FlowSummaryUtil.qll b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/FlowSummaryUtil.qll index a5df1d4716af..33f891935f42 100644 --- a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/FlowSummaryUtil.qll +++ b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/FlowSummaryUtil.qll @@ -49,3 +49,10 @@ string getAnArrayContent() { // Values stored at an unknown index result = "ArrayElement[?]" } + +/** + * Gets an argument position up to a certain limit. + * + * This can be used to generate flow summaries that should preserve such positions. + */ +int getAnArgumentPosition() { result = [0 .. 10] } diff --git a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Promises.qll b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Promises.qll index 7587ab11dc47..74048f0e397e 100644 --- a/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Promises.qll +++ b/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Promises.qll @@ -368,3 +368,29 @@ private class PromiseWithResolversLike extends SummarizedCallable { ) } } + +class PromiseTry extends DataFlow::SummarizedCallable { + PromiseTry() { this = "Promise.try()" } + + override DataFlow::CallNode getACallSimple() { + result = promiseConstructorRef().getAMemberCall(["try", "attempt"]) + or + result = DataFlow::moduleImport(["p-try", "es6-promise-try"]).getACall() + } + + override predicate propagatesFlow(string input, string output, boolean preservesValue) { + preservesValue = true and + ( + exists(int i | i = getAnArgumentPosition() | + input = "Argument[" + (i + 1) + "]" and + output = "Argument[0].Parameter[" + i + "]" + ) + or + input = "Argument[0].ReturnValue" and + output = "ReturnValue.Awaited" + or + input = "Argument[0].ReturnValue[exception]" and + output = "ReturnValue.Awaited[error]" + ) + } +} diff --git a/javascript/ql/src/change-notes/2025-09-16-promise-try-array-with.md b/javascript/ql/src/change-notes/2025-09-16-promise-try-array-with.md new file mode 100644 index 000000000000..a61a78d7f602 --- /dev/null +++ b/javascript/ql/src/change-notes/2025-09-16-promise-try-array-with.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Data flow is now tracked through the `Promise.try` and `Array.prototype.with` functions. diff --git a/javascript/ql/test/library-tests/TripleDot/array-with.js b/javascript/ql/test/library-tests/TripleDot/array-with.js new file mode 100644 index 000000000000..73c09a434ace --- /dev/null +++ b/javascript/ql/test/library-tests/TripleDot/array-with.js @@ -0,0 +1,25 @@ +function t1() { + const arr = [1, 2, 3]; + const newArr = arr.with(1, source('with.1')); + sink(newArr[1]); // $ hasValueFlow=with.1 +} + +function t2() { + const arr = [source('with.2.1'), 2, source('with.2.3')]; + const newArr = arr.with(1, 'replaced'); + sink(newArr[0]); // $ hasValueFlow=with.2.1 + sink(newArr[2]); // $ hasValueFlow=with.2.3 +} + +function t3() { + const arr = [1, 2, 3]; + const index = source('with.3.index'); + const newArr = arr.with(index, 'new value'); + // No assertions here as the index is tainted, not the value +} + +function t4() { + const arr = [1, 2, 3]; + const newArr = arr.with(1, source('with.4')); + sink(arr[1]); // This should NOT have value flow as with() returns a new array +} diff --git a/javascript/ql/test/library-tests/TripleDot/promise-try.js b/javascript/ql/test/library-tests/TripleDot/promise-try.js new file mode 100644 index 000000000000..9c96ee73426c --- /dev/null +++ b/javascript/ql/test/library-tests/TripleDot/promise-try.js @@ -0,0 +1,29 @@ +async function t1() { + const promise = Promise.try(() => { + return source('try.1'); + }); + sink(await promise); // $ hasValueFlow=try.1 +} + +async function t2() { + const promise = Promise.try((x) => { + return x + }, source('try.2')); + sink(await promise); // $ hasValueFlow=try.2 +} + +async function t3() { + const promise = Promise.try((x) => { + throw x; + }, source('try.3')); + promise.catch(err => { + sink(err); // $ hasValueFlow=try.3 + }); +} + +async function t4() { + const promise = Promise.try((x, y) => { + return y; + }, source('try.4.1'), source('try.4.2')); + sink(await promise); // $ hasValueFlow=try.4.2 +}