diff --git a/change-notes/1.26/analysis-javascript.md b/change-notes/1.26/analysis-javascript.md index 3864463508e..15edb607c70 100644 --- a/change-notes/1.26/analysis-javascript.md +++ b/change-notes/1.26/analysis-javascript.md @@ -4,6 +4,10 @@ * Angular-specific taint sources and sinks are now recognized by the security queries. +* Support for React has improved, with better handling of react hooks, react-router path parameters, lazy-loaded components, and components transformed using `react-redux` and/or `styled-components`. + +* Dynamic imports are now analyzed more precisely. + * Support for the following frameworks and libraries has been improved: - [@angular/*](https://www.npmjs.com/package/@angular/core) - [AWS Serverless](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html) @@ -29,7 +33,13 @@ - [needle](https://www.npmjs.com/package/needle) - [object-inspect](https://www.npmjs.com/package/object-inspect) - [pretty-format](https://www.npmjs.com/package/pretty-format) + - [react](https://www.npmjs.com/package/react) + - [react-router-dom](https://www.npmjs.com/package/react-router-dom) + - [react-redux](https://www.npmjs.com/package/react-redux) + - [redis](https://www.npmjs.com/package/redis) + - [redux](https://www.npmjs.com/package/redux) - [stringify-object](https://www.npmjs.com/package/stringify-object) + - [styled-components](https://www.npmjs.com/package/styled-components) - [throttle-debounce](https://www.npmjs.com/package/throttle-debounce) - [underscore](https://www.npmjs.com/package/underscore) diff --git a/javascript/ql/src/experimental/Security/CWE-94/ServerSideTemplateInjection.ql b/javascript/ql/src/experimental/Security/CWE-94/ServerSideTemplateInjection.ql index b4f728b6b15..9edce4d1932 100644 --- a/javascript/ql/src/experimental/Security/CWE-94/ServerSideTemplateInjection.ql +++ b/javascript/ql/src/experimental/Security/CWE-94/ServerSideTemplateInjection.ql @@ -57,6 +57,10 @@ class SSTINunjucksSink extends ServerSideTemplateInjectionSink { } } +class LodashTemplateSink extends ServerSideTemplateInjectionSink { + LodashTemplateSink() { this = LodashUnderscore::member("template").getACall().getArgument(0) } +} + from DataFlow::PathNode source, DataFlow::PathNode sink, ServerSideTemplateInjectionConfiguration c where c.hasFlowPath(source, sink) select sink.getNode(), source, sink, diff --git a/javascript/ql/src/meta/analysis-quality/TaintSteps.ql b/javascript/ql/src/meta/analysis-quality/TaintSteps.ql index fdafa5197ab..5e40d63fa3d 100644 --- a/javascript/ql/src/meta/analysis-quality/TaintSteps.ql +++ b/javascript/ql/src/meta/analysis-quality/TaintSteps.ql @@ -11,12 +11,22 @@ import javascript import CallGraphQuality -class BasicTaintConfiguration extends TaintTracking::Configuration { - BasicTaintConfiguration() { this = "BasicTaintConfiguration" } -} - predicate relevantStep(DataFlow::Node pred, DataFlow::Node succ) { - any(BasicTaintConfiguration cfg).isAdditionalFlowStep(pred, succ) and + ( + any(TaintTracking::AdditionalTaintStep dts).step(pred, succ) + or + any(DataFlow::AdditionalFlowStep cfg).step(pred, succ) + or + any(DataFlow::AdditionalFlowStep cfg).step(pred, succ, _, _) + or + any(DataFlow::AdditionalFlowStep cfg).loadStep(pred, succ, _) + or + any(DataFlow::AdditionalFlowStep cfg).storeStep(pred, succ, _) + or + any(DataFlow::AdditionalFlowStep cfg).loadStoreStep(pred, succ, _, _) + or + any(DataFlow::AdditionalFlowStep cfg).loadStoreStep(pred, succ, _) + ) and not pred.getFile() instanceof IgnoredFile and not succ.getFile() instanceof IgnoredFile } diff --git a/javascript/ql/src/semmle/javascript/Promises.qll b/javascript/ql/src/semmle/javascript/Promises.qll index 4f730b36f0a..650ed280b5e 100644 --- a/javascript/ql/src/semmle/javascript/Promises.qll +++ b/javascript/ql/src/semmle/javascript/Promises.qll @@ -641,3 +641,39 @@ private module ClosurePromise { override predicate step(DataFlow::Node src, DataFlow::Node dst) { src = pred and dst = this } } } + +private module DynamicImportSteps { + /** + * A step from an export value to its uses via dynamic imports. + * + * For example: + * ```js + * // foo.js + * export default Foo + * + * // bar.js + * let Foo = await import('./foo'); + * ``` + */ + class DynamicImportStep extends PreCallGraphStep { + override predicate storeStep(DataFlow::Node pred, DataFlow::SourceNode succ, string prop) { + exists(DynamicImportExpr imprt | + pred = imprt.getImportedModule().getAnExportedValue("default") and + succ = imprt.flow() and + prop = Promises::valueProp() + ) + } + + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + // Special-case code like `(await import(x)).Foo` to boost type tracking depth. + exists( + DynamicImportExpr imprt, string name, DataFlow::Node mid, DataFlow::SourceNode awaited + | + pred = imprt.getImportedModule().getAnExportedValue(name) and + mid.getALocalSource() = imprt.flow() and + PromiseFlow::loadStep(mid, awaited, Promises::valueProp()) and + succ = awaited.getAPropertyRead(name) + ) + } + } +} diff --git a/javascript/ql/src/semmle/javascript/dataflow/Sources.qll b/javascript/ql/src/semmle/javascript/dataflow/Sources.qll index c3017585c71..25390f3c7f9 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/Sources.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/Sources.qll @@ -307,7 +307,8 @@ module SourceNode { astNode instanceof FunctionBindExpr or astNode instanceof DynamicImportExpr or astNode instanceof ImportSpecifier or - astNode instanceof ImportMetaExpr + astNode instanceof ImportMetaExpr or + astNode instanceof TaggedTemplateExpr ) or DataFlow::parameterNode(this, _) diff --git a/javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll b/javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll index 6698800918c..04626e88106 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll @@ -327,7 +327,7 @@ module TaintTracking { * taint to flow from `v` to any read of `c2.state.p`, where `c2` * also is an instance of `C`. */ - private class ReactComponentStateTaintStep extends AdditionalTaintStep, DataFlow::ValueNode { + private class ReactComponentStateTaintStep extends AdditionalTaintStep { DataFlow::Node source; ReactComponentStateTaintStep() { @@ -358,7 +358,7 @@ module TaintTracking { * taint to flow from `v` to any read of `c2.props.p`, where `c2` * also is an instance of `C`. */ - private class ReactComponentPropsTaintStep extends AdditionalTaintStep, DataFlow::ValueNode { + private class ReactComponentPropsTaintStep extends AdditionalTaintStep { DataFlow::Node source; ReactComponentPropsTaintStep() { diff --git a/javascript/ql/src/semmle/javascript/frameworks/ComposedFunctions.qll b/javascript/ql/src/semmle/javascript/frameworks/ComposedFunctions.qll index 7f56771b8a8..df0b3ce73e3 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/ComposedFunctions.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/ComposedFunctions.qll @@ -5,31 +5,116 @@ import javascript /** - * A function composed from a collection of functions. + * A call to a function that constructs a function composition `f(g(h(...)))` from a + * series functions `f, g, h, ...`. */ -private class ComposedFunction extends DataFlow::CallNode { - ComposedFunction() { - exists(string name | - name = "just-compose" or - name = "compose-function" - | - this = DataFlow::moduleImport(name).getACall() - ) - or - this = LodashUnderscore::member("flow").getACall() +class FunctionCompositionCall extends DataFlow::CallNode { + FunctionCompositionCall::Range range; + + FunctionCompositionCall() { this = range } + + /** + * Gets the `i`th function in the composition `f(g(h(...)))`, counting from left to right. + * + * Note that this is the opposite of the order in which the function are invoked, + * that is, `g` occurs later than `f` in `f(g(...))` but is invoked before `f`. + */ + DataFlow::Node getOperandNode(int i) { result = range.getOperandNode(i) } + + /** Gets a node holding one of the functions to be composed. */ + final DataFlow::Node getAnOperandNode() { result = getOperandNode(_) } + + /** + * Gets the function flowing into the `i`th function in the composition `f(g(h(...)))`. + * + * Note that this is the opposite of the order in which the function are invoked, + * that is, `g` occurs later than `f` in `f(g(...))` but is invoked before `f`. + */ + final DataFlow::FunctionNode getOperandFunction(int i) { + result = getOperandNode(i).getALocalSource() + } + + /** Gets any of the functions being composed. */ + final DataFlow::Node getAnOperandFunction() { result = getOperandFunction(_) } + + /** Gets the number of functions being composed. */ + int getNumOperand() { result = range.getNumOperand() } +} + +/** + * Companion module to the `FunctionCompositionCall` class. + */ +module FunctionCompositionCall { + /** + * Class that determines the set of values in `FunctionCompositionCall`. + * + * May be subclassed to classify more calls as function compositions. + */ + abstract class Range extends DataFlow::CallNode { + /** + * Gets the function flowing into the `i`th function in the composition `f(g(h(...)))`. + */ + abstract DataFlow::Node getOperandNode(int i); + + /** Gets the number of functions being composed. */ + abstract int getNumOperand(); } /** - * Gets the ith function in this composition. + * A function composition call that accepts its operands in an array or + * via the arguments list. + * + * For simplicity, we model every composition function as if it supported this. */ - DataFlow::FunctionNode getFunction(int i) { result.flowsTo(getArgument(i)) } + abstract private class WithArrayOverloading extends Range { + /** Gets the `i`th argument to the call or the `i`th array element passed into the call. */ + DataFlow::Node getEffectiveArgument(int i) { + result = getArgument(0).(DataFlow::ArrayCreationNode).getElement(i) + or + not getArgument(0) instanceof DataFlow::ArrayCreationNode and + result = getArgument(i) + } + + override int getNumOperand() { + result = getArgument(0).(DataFlow::ArrayCreationNode).getSize() + or + not getArgument(0) instanceof DataFlow::ArrayCreationNode and + result = getNumArgument() + } + } + + /** A call whose arguments are functions `f,g,h` which are composed into `f(g(h(...))` */ + private class RightToLeft extends WithArrayOverloading { + RightToLeft() { + this = DataFlow::moduleImport(["compose-function"]).getACall() + or + this = DataFlow::moduleMember(["redux", "ramda"], "compose").getACall() + or + this = LodashUnderscore::member("flowRight").getACall() + } + + override DataFlow::Node getOperandNode(int i) { result = getEffectiveArgument(i) } + } + + /** A call whose arguments are functions `f,g,h` which are composed into `f(g(h(...))` */ + private class LeftToRight extends WithArrayOverloading { + LeftToRight() { + this = DataFlow::moduleImport("just-compose").getACall() + or + this = LodashUnderscore::member("flow").getACall() + } + + override DataFlow::Node getOperandNode(int i) { + result = getEffectiveArgument(getNumOperand() - i - 1) + } + } } /** * A taint step for a composed function. */ private class ComposedFunctionTaintStep extends TaintTracking::AdditionalTaintStep { - ComposedFunction composed; + FunctionCompositionCall composed; DataFlow::CallNode call; ComposedFunctionTaintStep() { @@ -38,25 +123,24 @@ private class ComposedFunctionTaintStep extends TaintTracking::AdditionalTaintSt } override predicate step(DataFlow::Node pred, DataFlow::Node succ) { - exists(int fnIndex, DataFlow::FunctionNode fn | fn = composed.getFunction(fnIndex) | - // flow out of the composed call - fnIndex = composed.getNumArgument() - 1 and - pred = fn.getAReturn() and - succ = this + exists(int fnIndex, DataFlow::FunctionNode fn | fn = composed.getOperandFunction(fnIndex) | + // flow into the first function + fnIndex = composed.getNumOperand() - 1 and + exists(int callArgIndex | + pred = call.getArgument(callArgIndex) and + succ = fn.getParameter(callArgIndex) + ) or - if fnIndex = 0 - then - // flow into the first composed function - exists(int callArgIndex | - pred = call.getArgument(callArgIndex) and - succ = fn.getParameter(callArgIndex) - ) - else - // flow through the composed functions - exists(DataFlow::FunctionNode predFn | predFn = composed.getFunction(fnIndex - 1) | - pred = predFn.getAReturn() and - succ = fn.getParameter(0) - ) + // flow through the composed functions + exists(DataFlow::FunctionNode predFn | predFn = composed.getOperandFunction(fnIndex + 1) | + pred = predFn.getReturnNode() and + succ = fn.getParameter(0) + ) + or + // flow out of the composed call + fnIndex = 0 and + pred = fn.getReturnNode() and + succ = this ) } } diff --git a/javascript/ql/src/semmle/javascript/frameworks/LodashUnderscore.qll b/javascript/ql/src/semmle/javascript/frameworks/LodashUnderscore.qll index 3e2b3b777b0..75066172874 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/LodashUnderscore.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/LodashUnderscore.qll @@ -407,7 +407,12 @@ module LodashUnderscore { "shuffle", "sample", "toArray", "partition", "compact", "first", "initial", "last", "rest", "flatten", "without", "difference", "uniq", "unique", "unzip", "transpose", "object", "chunk", "values", "mapObject", "pick", "omit", "defaults", "clone", "tap", - "identity"] and + "identity", + // String category + "camelCase", "capitalize", "deburr", "kebabCase", "lowerCase", "lowerFirst", "pad", + "padEnd", "padStart", "repeat", "replace", "snakeCase", "split", "startCase", "toLower", + "toUpper", "trim", "trimEnd", "trimStart", "truncate", "unescape", "upperCase", + "upperFirst", "words"] and pred = call.getArgument(0) and succ = call or diff --git a/javascript/ql/src/semmle/javascript/frameworks/React.qll b/javascript/ql/src/semmle/javascript/frameworks/React.qll index 3cf93056a8c..cfc77682291 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/React.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/React.qll @@ -3,6 +3,8 @@ */ import javascript +private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps +private import semmle.javascript.dataflow.internal.PreCallGraphStep /** * Gets a reference to the 'React' object. @@ -548,3 +550,229 @@ private class ReactJSXElement extends JSXElement { */ ReactComponent getComponent() { result = component } } + +/** + * Step through the state variable of a `useState` call. + * + * It returns a pair of the current state, and a callback to change the state. + * + * For example: + * ```js + * let [state, setState] = useState(initialValue); + * let [state, setState] = useState(() => initialValue); // lazy initial state + * + * setState(newState); + * setState(prevState => { ... }); + * ``` + */ +private class UseStateStep extends PreCallGraphStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(DataFlow::CallNode call | call = react().getAMemberCall("useState") | + pred = + [call.getArgument(0), // initial state + call.getCallback(0).getReturnNode(), // lazy initial state + call.getAPropertyRead("1").getACall().getArgument(0), // setState invocation + call.getAPropertyRead("1").getACall().getCallback(0).getReturnNode()] and // setState with callback + succ = call.getAPropertyRead("0") + or + // Propagate current state into the callback argument of `setState(prevState => { ... })` + pred = call.getAPropertyRead("0") and + succ = call.getAPropertyRead("1").getACall().getCallback(0).getParameter(0) + ) + } +} + +/** + * A step through a React context object. + * + * For example: + * ```js + * let MyContext = React.createContext('foo'); + * + * + * + * + * + * function Foo() { + * let succ = useContext(MyContext); + * } + * ``` + */ +private class UseContextStep extends PreCallGraphStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(DataFlow::CallNode context | + pred = getAContextInput(context) and + succ = getAContextOutput(context) + ) + } +} + +/** + * Gets a data flow node referring to the result of the given `createContext` call. + */ +private DataFlow::SourceNode getAContextRef(DataFlow::CallNode createContext) { + createContext = react().getAMemberCall("createContext") and + result = createContext + or + // Track through imports/exports, but not full type tracking, so this can be used as a PreCallGraphStep. + exists(DataFlow::Node mid | + getAContextRef(createContext).flowsTo(mid) and + FlowSteps::propertyFlowStep(mid, result) + ) +} + +/** + * Gets a data flow node whose value is provided to the given context object. + * + * For example: + * ```jsx + * React.createContext(x); + * + * ``` + */ +pragma[nomagic] +private DataFlow::Node getAContextInput(DataFlow::CallNode createContext) { + createContext = react().getAMemberCall("createContext") and + result = createContext.getArgument(0) // initial value + or + exists(JSXElement provider | + getAContextRef(createContext) + .getAPropertyRead("Provider") + .flowsTo(provider.getNameExpr().flow()) and + result = provider.getAttributeByName("value").getValue().flow() + ) +} + +/** + * Gets a data flow node whose value is obtained from the given context object. + * + * For example: + * ```js + * let value = useContext(MyContext); + * ``` + */ +pragma[nomagic] +private DataFlow::SourceNode getAContextOutput(DataFlow::CallNode createContext) { + exists(DataFlow::CallNode call | + call = react().getAMemberCall("useContext") and + getAContextRef(createContext).flowsTo(call.getArgument(0)) and + result = call + ) + or + exists(DataFlow::ClassNode cls | + getAContextRef(createContext).flowsTo(cls.getAPropertyWrite("contextType").getRhs()) and + result = cls.getAReceiverNode().getAPropertyRead("context") + ) +} + +/** + * A step through a `useMemo` call; for example: + * ```js + * let succ = useMemo(() => pred, []); + * ``` + */ +private class UseMemoStep extends PreCallGraphStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(DataFlow::CallNode call | call = react().getAMemberCall("useMemo") | + pred = call.getCallback(0).getReturnNode() and + succ = call + ) + } +} + +private DataFlow::SourceNode reactRouterDom() { + result = DataFlow::moduleImport("react-router-dom") +} + +private class ReactRouterSource extends RemoteFlowSource { + ReactRouterSource() { + this = reactRouterDom().getAMemberCall("useParams") + or + this = reactRouterDom().getAMemberCall("useRouteMatch").getAPropertyRead(["params", "url"]) + } + + override string getSourceType() { result = "react-router path parameters" } +} + +/** + * Holds if `mod` transitively depends on `react-router-dom`. + * + * We assume any React component in such a file may be used in a context where react-router + * injects the `location` property in its `props` object. + */ +private predicate dependsOnReactRouter(Module mod) { + mod.getAnImport().getImportedPath().getValue() = "react-router-dom" + or + dependsOnReactRouter(mod.getAnImportedModule()) +} + +/** + * A reference to the DOM location obtained through `react-router-dom` + * + * For example: + * ```js + * let location = useLocation(); + * + * function MyComponent(props) { + * props.location; + * } + * export default withRouter(MyComponent); + */ +private class ReactRouterLocationSource extends DOM::LocationSource::Range { + ReactRouterLocationSource() { + this = reactRouterDom().getAMemberCall("useLocation") + or + exists(ReactComponent component | + dependsOnReactRouter(component.getTopLevel()) and + this = component.getAPropRead("location") + ) + } +} + +/** + * Gets a reference to a function which, if called with a React component, returns wrapped + * version of that component, which we model as a direct reference to the underlying component. + */ +private DataFlow::SourceNode higherOrderComponentBuilder() { + // `memo(f)` returns a function that behaves as `f` but caches results + // It is sometimes used to wrap an entire functional component. + result = react().getAPropertyRead("memo") + or + result = DataFlow::moduleMember("react-redux", "connect").getACall() + or + result = reactRouterDom().getAPropertyRead("withRouter") + or + exists(FunctionCompositionCall compose | + higherOrderComponentBuilder().flowsTo(compose.getAnOperandNode()) and + result = compose + ) +} + +private class HigherOrderComponentStep extends PreCallGraphStep { + override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) { + // `lazy(() => P)` returns a proxy for the component eventually returned by + // the promise P. We model this call as simply returning the value in P. + // It is primarily used for lazy-loading of React components. + exists(DataFlow::CallNode call | + call = react().getAMemberCall("lazy") and + pred = call.getCallback(0).getReturnNode() and + succ = call and + prop = Promises::valueProp() + ) + } + + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(DataFlow::CallNode call | + call = higherOrderComponentBuilder().getACall() and + pred = call.getArgument(0) and + succ = call + ) + or + exists(TaggedTemplateExpr expr, DataFlow::CallNode call | + call = DataFlow::moduleImport("styled-components").getACall() and + pred = call.getArgument(0) and + call.flowsTo(expr.getTag().flow()) and + succ = expr.flow() + ) + } +} diff --git a/javascript/ql/test/library-tests/DataFlow/tests.expected b/javascript/ql/test/library-tests/DataFlow/tests.expected index 26d212bcf6f..b65daf255c0 100644 --- a/javascript/ql/test/library-tests/DataFlow/tests.expected +++ b/javascript/ql/test/library-tests/DataFlow/tests.expected @@ -1508,6 +1508,7 @@ sources | tst.js:50:14:53:3 | return of constructor of class A | | tst.js:51:5:51:13 | super(42) | | tst.js:58:1:58:3 | tag | +| tst.js:58:1:58:13 | tag `x: ${x}` | | tst.js:61:1:61:5 | ::o.m | | tst.js:61:3:61:5 | o.m | | tst.js:62:1:62:4 | o::g | diff --git a/javascript/ql/test/library-tests/frameworks/ComposedFunctions/compose.expected b/javascript/ql/test/library-tests/frameworks/ComposedFunctions/compose.expected index 52b552b3d38..932f4ea6d43 100644 --- a/javascript/ql/test/library-tests/frameworks/ComposedFunctions/compose.expected +++ b/javascript/ql/test/library-tests/frameworks/ComposedFunctions/compose.expected @@ -1,13 +1,13 @@ | tst.js:10:10:10:15 | source | | tst.js:15:10:15:13 | f1() | -| tst.js:20:10:20:23 | compose1(f2)() | -| tst.js:28:10:28:27 | compose1(f3, f4)() | -| tst.js:33:10:33:28 | compose1(o.f, f5)() | -| tst.js:41:10:41:27 | compose1(f6, f7)() | -| tst.js:49:10:49:33 | compose ... source) | -| tst.js:61:10:61:40 | compose ... source) | -| tst.js:66:10:66:30 | compose ... source) | +| tst.js:20:10:20:24 | lcompose1(f2)() | +| tst.js:28:10:28:28 | lcompose1(f3, f4)() | +| tst.js:33:10:33:29 | lcompose1(o.f, f5)() | +| tst.js:41:10:41:28 | lcompose1(f6, f7)() | +| tst.js:49:10:49:34 | lcompos ... source) | +| tst.js:61:10:61:41 | lcompos ... source) | +| tst.js:66:10:66:31 | lcompos ... source) | | tst.js:89:10:89:31 | f18(und ... source) | -| tst.js:94:10:94:24 | compose2(f19)() | -| tst.js:99:10:99:24 | compose3(f20)() | -| tst.js:104:10:104:24 | compose4(f21)() | \ No newline at end of file +| tst.js:94:10:94:30 | rcompos ... o.f)() | +| tst.js:99:10:99:30 | lcompos ... f20)() | +| tst.js:104:10:104:30 | lcompos ... f21)() | diff --git a/javascript/ql/test/library-tests/frameworks/ComposedFunctions/tst.js b/javascript/ql/test/library-tests/frameworks/ComposedFunctions/tst.js index e6c7a6804d4..95426079a03 100644 --- a/javascript/ql/test/library-tests/frameworks/ComposedFunctions/tst.js +++ b/javascript/ql/test/library-tests/frameworks/ComposedFunctions/tst.js @@ -1,8 +1,8 @@ -import compose1 from 'just-compose'; -import compose2 from 'compose-function'; -import compose3 from 'lodash.flow'; +import lcompose1 from 'just-compose'; +import rcompose2 from 'compose-function'; +import lcompose3 from 'lodash.flow'; import _ from 'lodash'; -var compose4 = _.flow; +var lcompose4 = _.flow; (function(){ var source = SOURCE(); @@ -17,7 +17,7 @@ var compose4 = _.flow; function f2(){ return source; } - SINK(compose1(f2)()); + SINK(lcompose1(f2)()); function f3(){ @@ -25,12 +25,12 @@ var compose4 = _.flow; function f4(){ return source; } - SINK(compose1(f3, f4)()); + SINK(lcompose1(f3, f4)()); function f5(){ return source; } - SINK(compose1(o.f, f5)()); + SINK(lcompose1(o.f, f5)()); function f6(){ return source; @@ -38,7 +38,7 @@ var compose4 = _.flow; function f7(x){ return x; } - SINK(compose1(f6, f7)()); + SINK(lcompose1(f6, f7)()); function f8(x){ return x; @@ -46,7 +46,7 @@ var compose4 = _.flow; function f9(x){ return x; } - SINK(compose1(f8, f9)(source)); + SINK(lcompose1(f8, f9)(source)); function f10(x){ @@ -58,12 +58,12 @@ var compose4 = _.flow; function f12(x){ return x; } - SINK(compose1(f10, f11, f12)(source)); + SINK(lcompose1(f10, f11, f12)(source)); function f13(x){ return x + 'foo' ; } - SINK(compose1(f13)(source)); + SINK(lcompose1(f13)(source)); function f14(){ return undefined; @@ -76,7 +76,7 @@ var compose4 = _.flow; function f16(){ return undefined; } - SINK(compose1(f15, f16)()); // NO FLOW + SINK(lcompose1(f15, f16)()); // NO FLOW function f17(x, y){ return y; @@ -91,16 +91,21 @@ var compose4 = _.flow; function f19(){ return source; } - SINK(compose2(f19)()); + SINK(rcompose2(f19, o.f)()); function f20(){ return source; } - SINK(compose3(f20)()); + SINK(lcompose3(f16, f20)()); function f21(){ return source; } - SINK(compose4(f21)()); + SINK(lcompose4(f16, f21)()); + + function f22(){ + return source; + } + SINK(lcompose3(f22, f16)()); // NO FLOW })(); diff --git a/javascript/ql/test/library-tests/frameworks/ReactJS/higherOrderComponent.jsx b/javascript/ql/test/library-tests/frameworks/ReactJS/higherOrderComponent.jsx new file mode 100644 index 00000000000..62dbe3d85f9 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/ReactJS/higherOrderComponent.jsx @@ -0,0 +1,24 @@ +import { memo } from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import styled from 'styled-components'; +import unknownFunction from 'somewhere'; + +import { MyComponent } from './exportedComponent'; + +const StyledComponent = styled(MyComponent)` + color: red; +`; + +function mapStateToProps(x) { + return x; +} +function mapDispatchToProps(x) { + return x; +} + +const withConnect = connect(mapStateToProps, mapDispatchToProps); + +const ConnectedComponent = compose(withConnect, unknownFunction)(StyledComponent); + +export default memo(ConnectedComponent); diff --git a/javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected b/javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected index 5f738e573f0..fba8084a937 100644 --- a/javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected @@ -227,6 +227,9 @@ test_ReactComponent_getACandidatePropsValue | props.js:30:46:30:67 | "propFr ... tProps" | | props.js:32:22:32:34 | "propFromJSX" | | props.js:34:33:34:53 | "propFr ... ructor" | +| useHigherOrderComponent.jsx:5:33:5:37 | "red" | +| useHigherOrderComponent.jsx:11:39:11:44 | "lazy" | +| useHigherOrderComponent.jsx:17:40:17:46 | "lazy2" | test_ReactComponent | es5.js:1:31:11:1 | {\\n dis ... ;\\n }\\n} | | es5.js:18:33:22:1 | {\\n ren ... ;\\n }\\n} | @@ -285,6 +288,9 @@ test_JSXname | thisAccesses.js:60:19:60:41 | | thisAccesses.js:60:20:60:28 | this.name | this.name | dot | | thisAccesses.js:61:19:61:41 | | thisAccesses.js:61:20:61:28 | this.this | this.this | dot | | thisAccesses_importedMappers.js:13:16:13:21 |
| thisAccesses_importedMappers.js:13:17:13:19 | div | div | Identifier | +| useHigherOrderComponent.jsx:5:12:5:39 | | useHigherOrderComponent.jsx:5:13:5:25 | SomeComponent | SomeComponent | Identifier | +| useHigherOrderComponent.jsx:11:12:11:46 | | useHigherOrderComponent.jsx:11:13:11:31 | LazyLoadedComponent | LazyLoadedComponent | Identifier | +| useHigherOrderComponent.jsx:17:12:17:48 | | useHigherOrderComponent.jsx:17:13:17:32 | LazyLoadedComponent2 | LazyLoadedComponent2 | Identifier | test_JSXName_this | es5.js:4:12:4:45 |
He ... }
| es5.js:4:24:4:27 | this | | es5.js:20:12:20:44 |

Hel ... e}

| es5.js:20:24:20:27 | this | diff --git a/javascript/ql/test/library-tests/frameworks/ReactJS/useHigherOrderComponent.jsx b/javascript/ql/test/library-tests/frameworks/ReactJS/useHigherOrderComponent.jsx new file mode 100644 index 00000000000..a57c5aa70ba --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/ReactJS/useHigherOrderComponent.jsx @@ -0,0 +1,18 @@ +import SomeComponent from './higherOrderComponent'; +import { lazy } from 'react'; + +function foo() { + return +} + +const LazyLoadedComponent = lazy(() => import('./higherOrderComponent')); + +function bar() { + return +} + +const LazyLoadedComponent2 = lazy(() => import('./exportedComponent').then(m => m.MyComponent)); + +function barz() { + return +} diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/Xss.expected b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/Xss.expected index a706a1b21d9..7c46c7dc041 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/Xss.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/Xss.expected @@ -174,6 +174,36 @@ nodes | react-native.js:8:18:8:24 | tainted | | react-native.js:9:27:9:33 | tainted | | react-native.js:9:27:9:33 | tainted | +| react-use-context.js:10:22:10:32 | window.name | +| react-use-context.js:10:22:10:32 | window.name | +| react-use-context.js:10:22:10:32 | window.name | +| react-use-context.js:16:26:16:36 | window.name | +| react-use-context.js:16:26:16:36 | window.name | +| react-use-context.js:16:26:16:36 | window.name | +| react-use-state.js:4:9:4:49 | state | +| react-use-state.js:4:10:4:14 | state | +| react-use-state.js:4:38:4:48 | window.name | +| react-use-state.js:4:38:4:48 | window.name | +| react-use-state.js:5:51:5:55 | state | +| react-use-state.js:5:51:5:55 | state | +| react-use-state.js:9:9:9:43 | state | +| react-use-state.js:9:10:9:14 | state | +| react-use-state.js:10:14:10:24 | window.name | +| react-use-state.js:10:14:10:24 | window.name | +| react-use-state.js:11:51:11:55 | state | +| react-use-state.js:11:51:11:55 | state | +| react-use-state.js:15:9:15:43 | state | +| react-use-state.js:15:10:15:14 | state | +| react-use-state.js:16:20:16:30 | window.name | +| react-use-state.js:16:20:16:30 | window.name | +| react-use-state.js:17:51:17:55 | state | +| react-use-state.js:17:51:17:55 | state | +| react-use-state.js:21:10:21:14 | state | +| react-use-state.js:22:14:22:17 | prev | +| react-use-state.js:23:35:23:38 | prev | +| react-use-state.js:23:35:23:38 | prev | +| react-use-state.js:25:20:25:30 | window.name | +| react-use-state.js:25:20:25:30 | window.name | | sanitiser.js:16:7:16:27 | tainted | | sanitiser.js:16:17:16:27 | window.name | | sanitiser.js:16:17:16:27 | window.name | @@ -737,6 +767,28 @@ edges | react-native.js:7:7:7:33 | tainted | react-native.js:9:27:9:33 | tainted | | react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | | react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | +| react-use-context.js:10:22:10:32 | window.name | react-use-context.js:10:22:10:32 | window.name | +| react-use-context.js:16:26:16:36 | window.name | react-use-context.js:16:26:16:36 | window.name | +| react-use-state.js:4:9:4:49 | state | react-use-state.js:5:51:5:55 | state | +| react-use-state.js:4:9:4:49 | state | react-use-state.js:5:51:5:55 | state | +| react-use-state.js:4:10:4:14 | state | react-use-state.js:4:9:4:49 | state | +| react-use-state.js:4:38:4:48 | window.name | react-use-state.js:4:10:4:14 | state | +| react-use-state.js:4:38:4:48 | window.name | react-use-state.js:4:10:4:14 | state | +| react-use-state.js:9:9:9:43 | state | react-use-state.js:11:51:11:55 | state | +| react-use-state.js:9:9:9:43 | state | react-use-state.js:11:51:11:55 | state | +| react-use-state.js:9:10:9:14 | state | react-use-state.js:9:9:9:43 | state | +| react-use-state.js:10:14:10:24 | window.name | react-use-state.js:9:10:9:14 | state | +| react-use-state.js:10:14:10:24 | window.name | react-use-state.js:9:10:9:14 | state | +| react-use-state.js:15:9:15:43 | state | react-use-state.js:17:51:17:55 | state | +| react-use-state.js:15:9:15:43 | state | react-use-state.js:17:51:17:55 | state | +| react-use-state.js:15:10:15:14 | state | react-use-state.js:15:9:15:43 | state | +| react-use-state.js:16:20:16:30 | window.name | react-use-state.js:15:10:15:14 | state | +| react-use-state.js:16:20:16:30 | window.name | react-use-state.js:15:10:15:14 | state | +| react-use-state.js:21:10:21:14 | state | react-use-state.js:22:14:22:17 | prev | +| react-use-state.js:22:14:22:17 | prev | react-use-state.js:23:35:23:38 | prev | +| react-use-state.js:22:14:22:17 | prev | react-use-state.js:23:35:23:38 | prev | +| react-use-state.js:25:20:25:30 | window.name | react-use-state.js:21:10:21:14 | state | +| react-use-state.js:25:20:25:30 | window.name | react-use-state.js:21:10:21:14 | state | | sanitiser.js:16:7:16:27 | tainted | sanitiser.js:23:29:23:35 | tainted | | sanitiser.js:16:7:16:27 | tainted | sanitiser.js:30:29:30:35 | tainted | | sanitiser.js:16:7:16:27 | tainted | sanitiser.js:33:29:33:35 | tainted | @@ -1140,6 +1192,12 @@ edges | optionalSanitizer.js:45:18:45:56 | sanitiz ... target | optionalSanitizer.js:26:16:26:32 | document.location | optionalSanitizer.js:45:18:45:56 | sanitiz ... target | Cross-site scripting vulnerability due to $@. | optionalSanitizer.js:26:16:26:32 | document.location | user-provided value | | react-native.js:8:18:8:24 | tainted | react-native.js:7:17:7:33 | req.param("code") | react-native.js:8:18:8:24 | tainted | Cross-site scripting vulnerability due to $@. | react-native.js:7:17:7:33 | req.param("code") | user-provided value | | react-native.js:9:27:9:33 | tainted | react-native.js:7:17:7:33 | req.param("code") | react-native.js:9:27:9:33 | tainted | Cross-site scripting vulnerability due to $@. | react-native.js:7:17:7:33 | req.param("code") | user-provided value | +| react-use-context.js:10:22:10:32 | window.name | react-use-context.js:10:22:10:32 | window.name | react-use-context.js:10:22:10:32 | window.name | Cross-site scripting vulnerability due to $@. | react-use-context.js:10:22:10:32 | window.name | user-provided value | +| react-use-context.js:16:26:16:36 | window.name | react-use-context.js:16:26:16:36 | window.name | react-use-context.js:16:26:16:36 | window.name | Cross-site scripting vulnerability due to $@. | react-use-context.js:16:26:16:36 | window.name | user-provided value | +| react-use-state.js:5:51:5:55 | state | react-use-state.js:4:38:4:48 | window.name | react-use-state.js:5:51:5:55 | state | Cross-site scripting vulnerability due to $@. | react-use-state.js:4:38:4:48 | window.name | user-provided value | +| react-use-state.js:11:51:11:55 | state | react-use-state.js:10:14:10:24 | window.name | react-use-state.js:11:51:11:55 | state | Cross-site scripting vulnerability due to $@. | react-use-state.js:10:14:10:24 | window.name | user-provided value | +| react-use-state.js:17:51:17:55 | state | react-use-state.js:16:20:16:30 | window.name | react-use-state.js:17:51:17:55 | state | Cross-site scripting vulnerability due to $@. | react-use-state.js:16:20:16:30 | window.name | user-provided value | +| react-use-state.js:23:35:23:38 | prev | react-use-state.js:25:20:25:30 | window.name | react-use-state.js:23:35:23:38 | prev | Cross-site scripting vulnerability due to $@. | react-use-state.js:25:20:25:30 | window.name | user-provided value | | sanitiser.js:23:21:23:44 | '' + ... '' | sanitiser.js:16:17:16:27 | window.name | sanitiser.js:23:21:23:44 | '' + ... '' | Cross-site scripting vulnerability due to $@. | sanitiser.js:16:17:16:27 | window.name | user-provided value | | sanitiser.js:30:21:30:44 | '' + ... '' | sanitiser.js:16:17:16:27 | window.name | sanitiser.js:30:21:30:44 | '' + ... '' | Cross-site scripting vulnerability due to $@. | sanitiser.js:16:17:16:27 | window.name | user-provided value | | sanitiser.js:33:21:33:44 | '' + ... '' | sanitiser.js:16:17:16:27 | window.name | sanitiser.js:33:21:33:44 | '' + ... '' | Cross-site scripting vulnerability due to $@. | sanitiser.js:16:17:16:27 | window.name | user-provided value | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/XssWithAdditionalSources.expected b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/XssWithAdditionalSources.expected index 549072f9c60..847b0c44aeb 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/XssWithAdditionalSources.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/XssWithAdditionalSources.expected @@ -174,6 +174,36 @@ nodes | react-native.js:8:18:8:24 | tainted | | react-native.js:9:27:9:33 | tainted | | react-native.js:9:27:9:33 | tainted | +| react-use-context.js:10:22:10:32 | window.name | +| react-use-context.js:10:22:10:32 | window.name | +| react-use-context.js:10:22:10:32 | window.name | +| react-use-context.js:16:26:16:36 | window.name | +| react-use-context.js:16:26:16:36 | window.name | +| react-use-context.js:16:26:16:36 | window.name | +| react-use-state.js:4:9:4:49 | state | +| react-use-state.js:4:10:4:14 | state | +| react-use-state.js:4:38:4:48 | window.name | +| react-use-state.js:4:38:4:48 | window.name | +| react-use-state.js:5:51:5:55 | state | +| react-use-state.js:5:51:5:55 | state | +| react-use-state.js:9:9:9:43 | state | +| react-use-state.js:9:10:9:14 | state | +| react-use-state.js:10:14:10:24 | window.name | +| react-use-state.js:10:14:10:24 | window.name | +| react-use-state.js:11:51:11:55 | state | +| react-use-state.js:11:51:11:55 | state | +| react-use-state.js:15:9:15:43 | state | +| react-use-state.js:15:10:15:14 | state | +| react-use-state.js:16:20:16:30 | window.name | +| react-use-state.js:16:20:16:30 | window.name | +| react-use-state.js:17:51:17:55 | state | +| react-use-state.js:17:51:17:55 | state | +| react-use-state.js:21:10:21:14 | state | +| react-use-state.js:22:14:22:17 | prev | +| react-use-state.js:23:35:23:38 | prev | +| react-use-state.js:23:35:23:38 | prev | +| react-use-state.js:25:20:25:30 | window.name | +| react-use-state.js:25:20:25:30 | window.name | | sanitiser.js:16:7:16:27 | tainted | | sanitiser.js:16:17:16:27 | window.name | | sanitiser.js:16:17:16:27 | window.name | @@ -741,6 +771,28 @@ edges | react-native.js:7:7:7:33 | tainted | react-native.js:9:27:9:33 | tainted | | react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | | react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | +| react-use-context.js:10:22:10:32 | window.name | react-use-context.js:10:22:10:32 | window.name | +| react-use-context.js:16:26:16:36 | window.name | react-use-context.js:16:26:16:36 | window.name | +| react-use-state.js:4:9:4:49 | state | react-use-state.js:5:51:5:55 | state | +| react-use-state.js:4:9:4:49 | state | react-use-state.js:5:51:5:55 | state | +| react-use-state.js:4:10:4:14 | state | react-use-state.js:4:9:4:49 | state | +| react-use-state.js:4:38:4:48 | window.name | react-use-state.js:4:10:4:14 | state | +| react-use-state.js:4:38:4:48 | window.name | react-use-state.js:4:10:4:14 | state | +| react-use-state.js:9:9:9:43 | state | react-use-state.js:11:51:11:55 | state | +| react-use-state.js:9:9:9:43 | state | react-use-state.js:11:51:11:55 | state | +| react-use-state.js:9:10:9:14 | state | react-use-state.js:9:9:9:43 | state | +| react-use-state.js:10:14:10:24 | window.name | react-use-state.js:9:10:9:14 | state | +| react-use-state.js:10:14:10:24 | window.name | react-use-state.js:9:10:9:14 | state | +| react-use-state.js:15:9:15:43 | state | react-use-state.js:17:51:17:55 | state | +| react-use-state.js:15:9:15:43 | state | react-use-state.js:17:51:17:55 | state | +| react-use-state.js:15:10:15:14 | state | react-use-state.js:15:9:15:43 | state | +| react-use-state.js:16:20:16:30 | window.name | react-use-state.js:15:10:15:14 | state | +| react-use-state.js:16:20:16:30 | window.name | react-use-state.js:15:10:15:14 | state | +| react-use-state.js:21:10:21:14 | state | react-use-state.js:22:14:22:17 | prev | +| react-use-state.js:22:14:22:17 | prev | react-use-state.js:23:35:23:38 | prev | +| react-use-state.js:22:14:22:17 | prev | react-use-state.js:23:35:23:38 | prev | +| react-use-state.js:25:20:25:30 | window.name | react-use-state.js:21:10:21:14 | state | +| react-use-state.js:25:20:25:30 | window.name | react-use-state.js:21:10:21:14 | state | | sanitiser.js:16:7:16:27 | tainted | sanitiser.js:23:29:23:35 | tainted | | sanitiser.js:16:7:16:27 | tainted | sanitiser.js:30:29:30:35 | tainted | | sanitiser.js:16:7:16:27 | tainted | sanitiser.js:33:29:33:35 | tainted | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-create-context.js b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-create-context.js new file mode 100644 index 00000000000..f07db51c411 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-create-context.js @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export let MyContext = createContext({root: null}); diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-provide-context.js b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-provide-context.js new file mode 100644 index 00000000000..8c2f9bdb557 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-provide-context.js @@ -0,0 +1,5 @@ +import { MyContext } from './react-create-context'; + +export function renderMain() { + return +} diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-context.js b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-context.js new file mode 100644 index 00000000000..6d7e20ec6eb --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-context.js @@ -0,0 +1,20 @@ +import { useContext, Component } from 'react'; +import { MyContext } from './react-create-context'; + +function useMyContext() { + return useContext(MyContext); +} + +export function useDoc1() { + let { root } = useMyContext(); + root.appendChild(window.name); // NOT OK +} + +class C extends Component { + foo() { + let { root } = this.context; + root.appendChild(window.name); // NOT OK + } +} + +C.contextType = MyContext; diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-state.js b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-state.js new file mode 100644 index 00000000000..672cd3bd689 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-state.js @@ -0,0 +1,33 @@ +import { useState } from 'react'; + +function initialState() { + let [state, setState] = useState(window.name); + return
; // NOT OK +} + +function setStateValue() { + let [state, setState] = useState('foo'); + setState(window.name); + return
; // NOT OK +} + +function setStateValueLazy() { + let [state, setState] = useState('foo'); + setState(() => window.name); + return
; // NOT OK +} + +function setStateValueLazy() { + let [state, setState] = useState('foo'); + setState(prev => { + document.body.innerHTML = prev; // NOT OK + }) + setState(() => window.name); +} + +function setStateValueSafe() { + let [state, setState] = useState('foo'); + setState('safe'); + setState(() => 'also safe'); + return
; // OK +}