Skip to content

JS: Model React 'use' and 'use server' #19852

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions javascript/ql/lib/ext/react.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extensions:
- addsTo:
pack: codeql/javascript-all
extensible: summaryModel
data:
- ["react", "Member[use]", "Argument[0].Awaited", "ReturnValue", "value"]
19 changes: 19 additions & 0 deletions javascript/ql/lib/semmle/javascript/frameworks/React.qll
Original file line number Diff line number Diff line change
Expand Up @@ -875,3 +875,22 @@ private class ReactPropAsViewComponentInput extends ViewComponentInput {

override string getSourceType() { result = "React props" }
}

private predicate isServerFunction(DataFlow::FunctionNode func) {
exists(Directive::UseServerDirective useServer |
useServer.getContainer() = func.getFunction()
or
useServer.getContainer().(Module).getAnExportedValue(_).getAFunctionValue() = func
)
}

private class ServerFunctionRemoteFlowSource extends RemoteFlowSource {
ServerFunctionRemoteFlowSource() {
exists(DataFlow::FunctionNode func |
isServerFunction(func) and
this = func.getAParameter()
)
}

override string getSourceType() { result = "React server function parameter" }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: majorAnalysis
---
* Taint is now tracked through the React `use` function.
* Parameters of React server functions, marked with the `"use server"` directive, are now seen as taint sources.
12 changes: 12 additions & 0 deletions javascript/ql/test/library-tests/TripleDot/react-use.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { use } from "react";

async function fetchData() {
return new Promise((resolve) => {
resolve(source("fetchedData"));
});
}

function Component() {
const data = use(fetchData());
sink(data); // $ hasValueFlow=fetchedData
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

10 changes: 5 additions & 5 deletions javascript/ql/test/library-tests/frameworks/ReactJS/es5.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
var Hello = React.createClass({
displayName: 'Hello',
render: function() {
return <div>Hello {this.props.name}</div>;
return <div>Hello {this.props.name}</div>; // $ threatModelSource=view-component-input
},
getDefaultProps: function() {
return {
name: 'world'
name: 'world' // $ getACandidatePropsValue
};
}
});
}); // $ reactComponent

Hello.info = function() {
return "Nothing to see here.";
Expand All @@ -17,6 +17,6 @@ Hello.info = function() {
var createReactClass = require('create-react-class');
var Greeting = createReactClass({
render: function() {
return <h1>Hello, {this.props.name}</h1>;
return <h1>Hello, {this.props.name}</h1>; // $ threatModelSource=view-component-input
}
});
}); // $ reactComponent
8 changes: 4 additions & 4 deletions javascript/ql/test/library-tests/frameworks/ReactJS/es6.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
class Hello extends React.Component {
class Hello extends React.Component { // $ threatModelSource=view-component-input
render() {
return <div>Hello {this.props.name}</div>;
return <div>Hello {this.props.name}</div>; // $ threatModelSource=view-component-input
}
static info() {
return "Nothing to see here.";
}
}
} // $ reactComponent
Hello.displayName = 'Hello';
Hello.defaultProps = {
name: 'world'
Expand All @@ -17,4 +17,4 @@ class Hello2 extends React.Component {
this.state.bar.foo = 42;
this.state = { baz: 42};
}
}
} // $ reactComponent
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function MyComponent(props) {
export function MyComponent(props) { // $ threatModelSource=view-component-input
return <div style={{color: props.color}}/>
}
} // $ reactComponent

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MyComponent } from "./exportedComponent";

export function render({color, location}) {
return <MyComponent color={color}/>
}
export function render({color, location}) { // $ threatModelSource=view-component-input locationSource threatModelSource=remote
return <MyComponent color={color}/> // $ getACandidatePropsValue
} // $ reactComponent
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component } from "react";

class C extends Component {}
class C extends Component {} // $ threatModelSource=view-component-input reactComponent

class D extends C {}
class D extends C {} // $ threatModelSource=view-component-input reactComponent
16 changes: 8 additions & 8 deletions javascript/ql/test/library-tests/frameworks/ReactJS/plainfn.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
function Hello(props) {
function Hello(props) { // $ threatModelSource=view-component-input
return <div>Hello {props.name}</div>;
}
} // $ reactComponent

function Hello2(props) {
function Hello2(props) { // $ threatModelSource=view-component-input
return React.createElement("div");
}
} // $ reactComponent

function Hello3(props) {
function Hello3(props) { // $ threatModelSource=view-component-input
var x = React.createElement("div");
return x;
}
} // $ reactComponent

function NotAComponent(props) {
if (y)
return React.createElement("div");
return g();
}

function SpuriousComponent(props) {
function SpuriousComponent(props) { // $ threatModelSource=view-component-input
if (y)
return React.createElement("div");
return 42;
}
} // $ reactComponent
10 changes: 5 additions & 5 deletions javascript/ql/test/library-tests/frameworks/ReactJS/preact.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
class Hello extends Preact.Component {
render(props, state) {
class Hello extends Preact.Component { // $ threatModelSource=view-component-input
render(props, state) { // $ threatModelSource=view-component-input
props.name;
state.name;
return <div/>;
}
}
} // $ reactComponent

class Hello extends preact.Component {
class Hello extends preact.Component { // $ threatModelSource=view-component-input

}
} // $ reactComponent
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Hello extends Component {
class Hello extends Component { // $ threatModelSource=view-component-input
render() {
this.props.name;
this.props.name; // $ threatModelSource=view-component-input
return <div/>;
}
}
} // $ reactComponent
28 changes: 14 additions & 14 deletions javascript/ql/test/library-tests/frameworks/ReactJS/props.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
function ES2015() {
class C extends React.Component {
}
class C extends React.Component { // $ threatModelSource=view-component-input
} // $ reactComponent

C.defaultProps = { propFromDefaultProps: "propFromDefaultProps" };
C.defaultProps = { propFromDefaultProps: "propFromDefaultProps" }; // $ getACandidatePropsValue

(<C propFromJSX={"propFromJSX"}/>);
(<C propFromJSX={"propFromJSX"}/>); // $ getACandidatePropsValue

new C({propFromConstructor: "propFromConstructor"});
new C({propFromConstructor: "propFromConstructor"}); // $ getACandidatePropsValue
}

function ES5() {
var C = React.createClass({
getDefaultProps() {
return { propFromDefaultProps: "propFromDefaultProps" };
return { propFromDefaultProps: "propFromDefaultProps" }; // $ getACandidatePropsValue
}
});
}); // $ reactComponent

(<C propFromJSX={"propFromJSX"}/>);
(<C propFromJSX={"propFromJSX"}/>); // $ getACandidatePropsValue

C({propFromConstructor: "propFromConstructor"});
C({propFromConstructor: "propFromConstructor"}); // $ getACandidatePropsValue

}

function Functional() {
function C(props) {
function C(props) { // $ threatModelSource=view-component-input
return <div/>;
}
} // $ reactComponent

C.defaultProps = { propFromDefaultProps: "propFromDefaultProps" };
C.defaultProps = { propFromDefaultProps: "propFromDefaultProps" }; // $ getACandidatePropsValue

(<C propFromJSX={"propFromJSX"}/>);
(<C propFromJSX={"propFromJSX"}/>); // $ getACandidatePropsValue

new C({propFromConstructor: "propFromConstructor"});
new C({propFromConstructor: "propFromConstructor"}); // $ getACandidatePropsValue

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class C extends React.Component {
class C extends React.Component { // $ threatModelSource=view-component-input
static getDerivedStateFromProps(props, state) {
return {};
}
Expand All @@ -8,4 +8,4 @@ class C extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
return {};
}
}
} // $ reactComponent

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ class Reads extends React.Component {
componentDidUpdate(prevProps, prevState) {
prevState.p4;
}
}
} // $ reactComponent
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ class Writes extends React.Component {
state = {
p7: 42
};
}
} // $ reactComponent

React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
return <div>Hello {this.props.name}</div>; // $ threatModelSource=view-component-input
},
getInitialState: function() {
return {
p8: 42
};
}
});
}); // $ reactComponent
Loading