From d116b424f42599019086fdd31f3e0aa7eddb75c2 Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Mon, 26 Oct 2020 15:09:53 +0000 Subject: [PATCH] JS: Add model of react hooks and react-router --- .../semmle/javascript/dataflow/Sources.qll | 3 +- .../javascript/dataflow/TaintTracking.qll | 4 +- .../semmle/javascript/frameworks/React.qll | 227 ++++++++++++++++++ .../ReactJS/higherOrderComponent.jsx | 24 ++ .../frameworks/ReactJS/tests.expected | 6 + .../ReactJS/useHigherOrderComponent.jsx | 18 ++ .../Security/CWE-079/DomBasedXss/Xss.expected | 53 ++++ .../XssWithAdditionalSources.expected | 48 ++++ .../DomBasedXss/react-create-context.js | 3 + .../DomBasedXss/react-provide-context.js | 5 + .../CWE-079/DomBasedXss/react-use-context.js | 11 + .../CWE-079/DomBasedXss/react-use-state.js | 33 +++ 12 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 javascript/ql/test/library-tests/frameworks/ReactJS/higherOrderComponent.jsx create mode 100644 javascript/ql/test/library-tests/frameworks/ReactJS/useHigherOrderComponent.jsx create mode 100644 javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-create-context.js create mode 100644 javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-provide-context.js create mode 100644 javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-context.js create mode 100644 javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-state.js 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/React.qll b/javascript/ql/src/semmle/javascript/frameworks/React.qll index 3cf93056a8c..e854f90d375 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,228 @@ 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) { + 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::CallNode getAContextOutput(DataFlow::CallNode createContext) { + result = react().getAMemberCall("useContext") and + getAContextRef(createContext).flowsTo(result.getArgument(0)) + 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() { + 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) { + // `memo(f)` returns a function behaves as `f` but caches results + // It is sometimes used to wrap an entire functional component. + 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/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 1cbde553e3a..60ebc15c973 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 @@ -144,6 +144,33 @@ 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-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 | @@ -683,6 +710,27 @@ 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-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 | @@ -1080,6 +1128,11 @@ 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-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 47a8a8737dc..9ff31455179 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 @@ -144,6 +144,33 @@ 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-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 | @@ -687,6 +714,27 @@ 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-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..819362f44fb --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/react-use-context.js @@ -0,0 +1,11 @@ +import { useContext } 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 +} 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 +}