Skip to content

Commit d116b42

Browse files
committed
JS: Add model of react hooks and react-router
1 parent 42c03ab commit d116b42

File tree

12 files changed

+432
-3
lines changed

12 files changed

+432
-3
lines changed

javascript/ql/src/semmle/javascript/dataflow/Sources.qll

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,8 @@ module SourceNode {
307307
astNode instanceof FunctionBindExpr or
308308
astNode instanceof DynamicImportExpr or
309309
astNode instanceof ImportSpecifier or
310-
astNode instanceof ImportMetaExpr
310+
astNode instanceof ImportMetaExpr or
311+
astNode instanceof TaggedTemplateExpr
311312
)
312313
or
313314
DataFlow::parameterNode(this, _)

javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ module TaintTracking {
327327
* taint to flow from `v` to any read of `c2.state.p`, where `c2`
328328
* also is an instance of `C`.
329329
*/
330-
private class ReactComponentStateTaintStep extends AdditionalTaintStep, DataFlow::ValueNode {
330+
private class ReactComponentStateTaintStep extends AdditionalTaintStep {
331331
DataFlow::Node source;
332332

333333
ReactComponentStateTaintStep() {
@@ -358,7 +358,7 @@ module TaintTracking {
358358
* taint to flow from `v` to any read of `c2.props.p`, where `c2`
359359
* also is an instance of `C`.
360360
*/
361-
private class ReactComponentPropsTaintStep extends AdditionalTaintStep, DataFlow::ValueNode {
361+
private class ReactComponentPropsTaintStep extends AdditionalTaintStep {
362362
DataFlow::Node source;
363363

364364
ReactComponentPropsTaintStep() {

javascript/ql/src/semmle/javascript/frameworks/React.qll

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44

55
import javascript
6+
private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps
7+
private import semmle.javascript.dataflow.internal.PreCallGraphStep
68

79
/**
810
* Gets a reference to the 'React' object.
@@ -548,3 +550,228 @@ private class ReactJSXElement extends JSXElement {
548550
*/
549551
ReactComponent getComponent() { result = component }
550552
}
553+
554+
/**
555+
* Step through the state variable of a `useState` call.
556+
*
557+
* It returns a pair of the current state, and a callback to change the state.
558+
*
559+
* For example:
560+
* ```js
561+
* let [state, setState] = useState(initialValue);
562+
* let [state, setState] = useState(() => initialValue); // lazy initial state
563+
*
564+
* setState(newState);
565+
* setState(prevState => { ... });
566+
* ```
567+
*/
568+
private class UseStateStep extends PreCallGraphStep {
569+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
570+
exists(DataFlow::CallNode call | call = react().getAMemberCall("useState") |
571+
pred =
572+
[call.getArgument(0), // initial state
573+
call.getCallback(0).getReturnNode(), // lazy initial state
574+
call.getAPropertyRead("1").getACall().getArgument(0), // setState invocation
575+
call.getAPropertyRead("1").getACall().getCallback(0).getReturnNode()] and // setState with callback
576+
succ = call.getAPropertyRead("0")
577+
or
578+
// Propagate current state into the callback argument of `setState(prevState => { ... })`
579+
pred = call.getAPropertyRead("0") and
580+
succ = call.getAPropertyRead("1").getACall().getCallback(0).getParameter(0)
581+
)
582+
}
583+
}
584+
585+
/**
586+
* A step through a React context object.
587+
*
588+
* For example:
589+
* ```js
590+
* let MyContext = React.createContext('foo');
591+
*
592+
* <MyContext.Provider value={pred}>
593+
* <Foo/>
594+
* </MyContext.Provider>
595+
*
596+
* function Foo() {
597+
* let succ = useContext(MyContext);
598+
* }
599+
*/
600+
private class UseContextStep extends PreCallGraphStep {
601+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
602+
exists(DataFlow::CallNode context |
603+
pred = getAContextInput(context) and
604+
succ = getAContextOutput(context)
605+
)
606+
}
607+
}
608+
609+
/**
610+
* Gets a data flow node referring to the result of the given `createContext` call.
611+
*/
612+
private DataFlow::SourceNode getAContextRef(DataFlow::CallNode createContext) {
613+
createContext = react().getAMemberCall("createContext") and
614+
result = createContext
615+
or
616+
// Track through imports/exports, but not full type tracking, so this can be used as a PreCallGraphStep.
617+
exists(DataFlow::Node mid |
618+
getAContextRef(createContext).flowsTo(mid) and
619+
FlowSteps::propertyFlowStep(mid, result)
620+
)
621+
}
622+
623+
/**
624+
* Gets a data flow node whose value is provided to the given context object.
625+
*
626+
* For example:
627+
* ```jsx
628+
* React.createContext(x);
629+
* <MyContext.Provider value={x}>
630+
* ```
631+
*/
632+
pragma[nomagic]
633+
private DataFlow::Node getAContextInput(DataFlow::CallNode createContext) {
634+
result = createContext.getArgument(0) // initial value
635+
or
636+
exists(JSXElement provider |
637+
getAContextRef(createContext)
638+
.getAPropertyRead("Provider")
639+
.flowsTo(provider.getNameExpr().flow()) and
640+
result = provider.getAttributeByName("value").getValue().flow()
641+
)
642+
}
643+
644+
/**
645+
* Gets a data flow node whose value is obtained from the given context object.
646+
*
647+
* For example:
648+
* ```js
649+
* let value = useContext(MyContext);
650+
* ```
651+
*/
652+
pragma[nomagic]
653+
private DataFlow::CallNode getAContextOutput(DataFlow::CallNode createContext) {
654+
result = react().getAMemberCall("useContext") and
655+
getAContextRef(createContext).flowsTo(result.getArgument(0))
656+
or
657+
exists(DataFlow::ClassNode cls |
658+
getAContextRef(createContext).flowsTo(cls.getAPropertyWrite("contextType").getRhs()) and
659+
result = cls.getAReceiverNode().getAPropertyRead("context")
660+
)
661+
}
662+
663+
/**
664+
* A step through a `useMemo` call; for example:
665+
* ```js
666+
* let succ = useMemo(() => pred, []);
667+
* ```
668+
*/
669+
private class UseMemoStep extends PreCallGraphStep {
670+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
671+
exists(DataFlow::CallNode call |
672+
call = react().getAMemberCall("useMemo")
673+
|
674+
pred = call.getCallback(0).getReturnNode() and
675+
succ = call
676+
)
677+
}
678+
}
679+
680+
private DataFlow::SourceNode reactRouterDom() {
681+
result = DataFlow::moduleImport("react-router-dom")
682+
}
683+
684+
private class ReactRouterSource extends RemoteFlowSource {
685+
ReactRouterSource() {
686+
this = reactRouterDom().getAMemberCall("useParams")
687+
or
688+
this = reactRouterDom().getAMemberCall("useRouteMatch").getAPropertyRead(["params", "url"])
689+
}
690+
691+
override string getSourceType() {
692+
result = "react-router path parameters"
693+
}
694+
}
695+
696+
/**
697+
* Holds if `mod` transitively depends on `react-router-dom`.
698+
*
699+
* We assume any React component in such a file may be used in a context where react-router
700+
* injects the `location` property in its `props` object.
701+
*/
702+
private predicate dependsOnReactRouter(Module mod) {
703+
mod.getAnImport().getImportedPath().getValue() = "react-router-dom"
704+
or
705+
dependsOnReactRouter(mod.getAnImportedModule())
706+
}
707+
708+
/**
709+
* A reference to the DOM location obtained through `react-router-dom`
710+
*
711+
* For example:
712+
* ```js
713+
* let location = useLocation();
714+
*
715+
* function MyComponent(props) {
716+
* props.location;
717+
* }
718+
* export default withRouter(MyComponent);
719+
*/
720+
private class ReactRouterLocationSource extends DOM::LocationSource::Range {
721+
ReactRouterLocationSource() {
722+
this = reactRouterDom().getAMemberCall("useLocation")
723+
or
724+
exists(ReactComponent component |
725+
dependsOnReactRouter(component.getTopLevel()) and
726+
this = component.getAPropRead("location")
727+
)
728+
}
729+
}
730+
731+
/**
732+
* Gets a reference to a function which, if called with a React component, returns wrapped
733+
* version of that component, which we model as a direct reference to the underlying component.
734+
*/
735+
private DataFlow::SourceNode higherOrderComponentBuilder() {
736+
result = react().getAPropertyRead("memo")
737+
or
738+
result = DataFlow::moduleMember("react-redux", "connect").getACall()
739+
or
740+
result = reactRouterDom().getAPropertyRead("withRouter")
741+
or
742+
exists(FunctionCompositionCall compose |
743+
higherOrderComponentBuilder().flowsTo(compose.getAnOperandNode()) and
744+
result = compose
745+
)
746+
}
747+
748+
private class HigherOrderComponentStep extends PreCallGraphStep {
749+
override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
750+
// `lazy(() => P)` returns a proxy for the component eventually returned by
751+
// the promise P. We model this call as simply returning the value in P.
752+
// It is primarily used for lazy-loading of React components.
753+
exists(DataFlow::CallNode call |
754+
call = react().getAMemberCall("lazy") and
755+
pred = call.getCallback(0).getReturnNode() and
756+
succ = call and
757+
prop = Promises::valueProp()
758+
)
759+
}
760+
761+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
762+
// `memo(f)` returns a function behaves as `f` but caches results
763+
// It is sometimes used to wrap an entire functional component.
764+
exists(DataFlow::CallNode call |
765+
call = higherOrderComponentBuilder().getACall() and
766+
pred = call.getArgument(0) and
767+
succ = call
768+
)
769+
or
770+
exists(TaggedTemplateExpr expr, DataFlow::CallNode call |
771+
call = DataFlow::moduleImport("styled-components").getACall() and
772+
pred = call.getArgument(0) and
773+
call.flowsTo(expr.getTag().flow()) and
774+
succ = expr.flow()
775+
)
776+
}
777+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { memo } from 'react';
2+
import { connect } from 'react-redux';
3+
import { compose } from 'redux';
4+
import styled from 'styled-components';
5+
import unknownFunction from 'somewhere';
6+
7+
import { MyComponent } from './exportedComponent';
8+
9+
const StyledComponent = styled(MyComponent)`
10+
color: red;
11+
`;
12+
13+
function mapStateToProps(x) {
14+
return x;
15+
}
16+
function mapDispatchToProps(x) {
17+
return x;
18+
}
19+
20+
const withConnect = connect(mapStateToProps, mapDispatchToProps);
21+
22+
const ConnectedComponent = compose(withConnect, unknownFunction)(StyledComponent);
23+
24+
export default memo(ConnectedComponent);

javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ test_ReactComponent_getACandidatePropsValue
227227
| props.js:30:46:30:67 | "propFr ... tProps" |
228228
| props.js:32:22:32:34 | "propFromJSX" |
229229
| props.js:34:33:34:53 | "propFr ... ructor" |
230+
| useHigherOrderComponent.jsx:5:33:5:37 | "red" |
231+
| useHigherOrderComponent.jsx:11:39:11:44 | "lazy" |
232+
| useHigherOrderComponent.jsx:17:40:17:46 | "lazy2" |
230233
test_ReactComponent
231234
| es5.js:1:31:11:1 | {\\n dis ... ;\\n }\\n} |
232235
| es5.js:18:33:22:1 | {\\n ren ... ;\\n }\\n} |
@@ -285,6 +288,9 @@ test_JSXname
285288
| thisAccesses.js:60:19:60:41 | <this.n ... s.name> | thisAccesses.js:60:20:60:28 | this.name | this.name | dot |
286289
| thisAccesses.js:61:19:61:41 | <this.t ... s.this> | thisAccesses.js:61:20:61:28 | this.this | this.this | dot |
287290
| thisAccesses_importedMappers.js:13:16:13:21 | <div/> | thisAccesses_importedMappers.js:13:17:13:19 | div | div | Identifier |
291+
| useHigherOrderComponent.jsx:5:12:5:39 | <SomeCo ... "red"/> | useHigherOrderComponent.jsx:5:13:5:25 | SomeComponent | SomeComponent | Identifier |
292+
| useHigherOrderComponent.jsx:11:12:11:46 | <LazyLo ... lazy"/> | useHigherOrderComponent.jsx:11:13:11:31 | LazyLoadedComponent | LazyLoadedComponent | Identifier |
293+
| useHigherOrderComponent.jsx:17:12:17:48 | <LazyLo ... azy2"/> | useHigherOrderComponent.jsx:17:13:17:32 | LazyLoadedComponent2 | LazyLoadedComponent2 | Identifier |
288294
test_JSXName_this
289295
| es5.js:4:12:4:45 | <div>He ... }</div> | es5.js:4:24:4:27 | this |
290296
| es5.js:20:12:20:44 | <h1>Hel ... e}</h1> | es5.js:20:24:20:27 | this |
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import SomeComponent from './higherOrderComponent';
2+
import { lazy } from 'react';
3+
4+
function foo() {
5+
return <SomeComponent color="red"/>
6+
}
7+
8+
const LazyLoadedComponent = lazy(() => import('./higherOrderComponent'));
9+
10+
function bar() {
11+
return <LazyLoadedComponent color="lazy"/>
12+
}
13+
14+
const LazyLoadedComponent2 = lazy(() => import('./exportedComponent').then(m => m.MyComponent));
15+
16+
function barz() {
17+
return <LazyLoadedComponent2 color="lazy2"/>
18+
}

0 commit comments

Comments
 (0)