Skip to content

Commit 15049ca

Browse files
authored
Merge pull request github#5183 from erik-krogh/next
Approved by asgerf
2 parents f9365dc + 55985c9 commit 15049ca

File tree

20 files changed

+511
-11
lines changed

20 files changed

+511
-11
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lgtm,codescanning
2+
* Support for [Next.js](https://www.npmjs.com/package/next) has been added.
3+
Taint sources, sinks, and steps are now recognized.

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import semmle.javascript.frameworks.Logging
9797
import semmle.javascript.frameworks.HttpFrameworks
9898
import semmle.javascript.frameworks.HttpProxy
9999
import semmle.javascript.frameworks.Markdown
100+
import semmle.javascript.frameworks.Next
100101
import semmle.javascript.frameworks.NoSQL
101102
import semmle.javascript.frameworks.PkgCloud
102103
import semmle.javascript.frameworks.PropertyProjection

javascript/ql/src/semmle/javascript/DOM.qll

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,4 +491,11 @@ module DOM {
491491
or
492492
result.hasUnderlyingType("Document")
493493
}
494+
495+
/**
496+
* Holds if a value assigned to property `name` of a DOM node can be interpreted as JavaScript via the `javascript:` protocol.
497+
*/
498+
string getAPropertyNameInterpretedAsJavaScriptUrl() {
499+
result = ["action", "formaction", "href", "src", "data"]
500+
}
494501
}

javascript/ql/src/semmle/javascript/JSX.qll

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ class JSXElement extends JSXNode {
6565
}
6666

6767
override string getAPrimaryQlClass() { result = "JSXElement" }
68+
69+
/**
70+
* Holds if this JSX element is a HTML element.
71+
* That is, the name starts with a lowercase letter.
72+
*/
73+
predicate isHTMLElement() { getName().regexpMatch("[a-z].*") }
6874
}
6975

7076
/**
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/**
2+
* Provides classes and predicates for reasoning about [Next.js](https://www.npmjs.com/package/next).
3+
*/
4+
5+
import javascript
6+
7+
/**
8+
* Provides classes and predicates modelling [Next.js](https://www.npmjs.com/package/next).
9+
*/
10+
module NextJS {
11+
/**
12+
* Gets a `package.json` that depends on the `Next.js` library.
13+
*/
14+
PackageJSON getANextPackage() { result.getDependencies().getADependency("next", _) }
15+
16+
/**
17+
* Gets a "pages" folder in a `Next.js` application.
18+
* JavaScript files inside these folders are mapped to routes.
19+
*/
20+
Folder getAPagesFolder() {
21+
result = getANextPackage().getFile().getParentContainer().getFolder("pages")
22+
or
23+
result = getAPagesFolder().getAFolder()
24+
}
25+
26+
/**
27+
* Gets a module corrosponding to a `Next.js` page.
28+
*/
29+
Module getAPagesModule() { result.getFile().getParentContainer() = getAPagesFolder() }
30+
31+
/**
32+
* Gets a module inside a "pages" folder where `fallback` from `getStaticPaths` is not set to false.
33+
* In such a module the `getStaticProps` method can be called with user-defined parameters.
34+
* If `fallback` is set to false, then only values defined by `getStaticPaths` are allowed.
35+
*/
36+
Module getAModuleWithFallbackPaths() {
37+
result = getAPagesModule() and
38+
exists(DataFlow::FunctionNode staticPaths, Expr fallback |
39+
staticPaths = result.getAnExportedValue("getStaticPaths").getAFunctionValue() and
40+
fallback =
41+
staticPaths.getAReturn().getALocalSource().getAPropertyWrite("fallback").getRhs().asExpr() and
42+
not fallback.(BooleanLiteral).getValue() = "false"
43+
)
44+
}
45+
46+
/**
47+
* User defined path parameter in `Next.js`.
48+
*/
49+
class NextParams extends RemoteFlowSource {
50+
NextParams() {
51+
this =
52+
getAModuleWithFallbackPaths()
53+
.getAnExportedValue("getStaticProps")
54+
.getAFunctionValue()
55+
.getParameter(0)
56+
.getAPropertyRead("params")
57+
}
58+
59+
override string getSourceType() { result = "Next request parameter" }
60+
}
61+
62+
/**
63+
* Gets the `getStaticProps` function in a Next.js page.
64+
* This function is executed at build time, or when a page with a new URL is requested for the first time (if `fallback` is not false).
65+
*/
66+
DataFlow::FunctionNode getStaticPropsFunction(Module pageModule) {
67+
pageModule = getAPagesModule() and
68+
result = pageModule.getAnExportedValue("getStaticProps").getAFunctionValue()
69+
}
70+
71+
/**
72+
* Gets the `getServerSideProps` function in a Next.js page.
73+
* This function is executed on the server every time a request for the page is made.
74+
* The function receives a context parameter, which includes HTTP request/response objects.
75+
*/
76+
DataFlow::FunctionNode getServerSidePropsFunction(Module pageModule) {
77+
pageModule = getAPagesModule() and
78+
result = pageModule.getAnExportedValue("getServerSideProps").getAFunctionValue()
79+
}
80+
81+
/**
82+
* Gets the `getInitialProps` function in a Next.js page.
83+
* This function is executed on the server every time a request for the page is made.
84+
* The function receives a context parameter, which includes HTTP request/response objects.
85+
*/
86+
DataFlow::FunctionNode getInitialProps(Module pageModule) {
87+
pageModule = getAPagesModule() and
88+
(
89+
result =
90+
pageModule
91+
.getAnExportedValue("default")
92+
.getAFunctionValue()
93+
.getAPropertyWrite("getInitialProps")
94+
.getRhs()
95+
.getAFunctionValue()
96+
or
97+
result =
98+
pageModule
99+
.getAnExportedValue("default")
100+
.getALocalSource()
101+
.getAstNode()
102+
.(ReactComponent)
103+
.getStaticMethod("getInitialProps")
104+
.flow()
105+
)
106+
}
107+
108+
/**
109+
* Gets a reference to a `props` object computed by the Next.js server.
110+
* This `props` object is both used both by the server and client to render the page.
111+
*/
112+
DataFlow::Node getAPropsSource(Module pageModule) {
113+
pageModule = getAPagesModule() and
114+
(
115+
result =
116+
[getStaticPropsFunction(pageModule), getServerSidePropsFunction(pageModule)]
117+
.getAReturn()
118+
.getALocalSource()
119+
.getAPropertyWrite("props")
120+
.getRhs()
121+
or
122+
result = getInitialProps(pageModule).getAReturn()
123+
)
124+
}
125+
126+
/**
127+
* A step modelling the flow from the server-computed props object to the default exported function that renders the page.
128+
*/
129+
class NextJSStaticPropsStep extends DataFlow::AdditionalFlowStep, DataFlow::FunctionNode {
130+
Module pageModule;
131+
132+
NextJSStaticPropsStep() {
133+
pageModule = getAPagesModule() and
134+
this = pageModule.getAnExportedValue("default").getAFunctionValue()
135+
}
136+
137+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
138+
pred = getAPropsSource(pageModule) and
139+
succ = this.getParameter(0)
140+
}
141+
}
142+
143+
/**
144+
* A step modelling the flow from the server-computed props object to the default exported React component that renders the page.
145+
*/
146+
class NextJSStaticReactComponentPropsStep extends DataFlow::AdditionalFlowStep,
147+
DataFlow::ValueNode {
148+
Module pageModule;
149+
ReactComponent component;
150+
151+
NextJSStaticReactComponentPropsStep() {
152+
pageModule = getAPagesModule() and
153+
this.getAstNode() = component and
154+
this = pageModule.getAnExportedValue("default").getALocalSource()
155+
}
156+
157+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
158+
pred = getAPropsSource(pageModule) and
159+
succ = component.getADirectPropsAccess()
160+
}
161+
}
162+
163+
/**
164+
* A Next.js function that is exected on the server for every request, seen as a routehandler.
165+
*/
166+
class NextHttpRouteHandler extends HTTP::Servers::StandardRouteHandler, DataFlow::FunctionNode {
167+
Module pageModule;
168+
169+
NextHttpRouteHandler() {
170+
this = getServerSidePropsFunction(pageModule) or this = getInitialProps(pageModule)
171+
}
172+
}
173+
174+
/**
175+
* A NodeJS HTTP request object in a Next.js page.
176+
*/
177+
class NextHttpRequestSource extends NodeJSLib::RequestSource {
178+
NextHttpRouteHandler rh;
179+
180+
NextHttpRequestSource() { this = rh.getParameter(0).getAPropertyRead("req") }
181+
182+
override HTTP::RouteHandler getRouteHandler() { result = rh }
183+
}
184+
185+
/**
186+
* A NodeJS HTTP response object in a Next.js page.
187+
*/
188+
class NextHttpResponseSource extends NodeJSLib::ResponseSource {
189+
NextHttpRouteHandler rh;
190+
191+
NextHttpResponseSource() { this = rh.getParameter(0).getAPropertyRead("res") }
192+
193+
override HTTP::RouteHandler getRouteHandler() { result = rh }
194+
}
195+
196+
/**
197+
* Gets a folder that contains API endpoints for a Next.js application.
198+
* These API endpoints act as Express-like route-handlers.
199+
*/
200+
Folder apiFolder() {
201+
result = getANextPackage().getFile().getParentContainer().getFolder("pages").getFolder("api")
202+
or
203+
result = apiFolder().getAFolder()
204+
}
205+
206+
/**
207+
* A Next.js route handler for an API endpoint.
208+
* The response (res) includes a set of Express.js-like methods,
209+
* and we therefore model the routehandler as an Express.js routehandler.
210+
*/
211+
class NextAPIRouteHandler extends DataFlow::FunctionNode, Express::RouteHandler,
212+
HTTP::Servers::StandardRouteHandler {
213+
NextAPIRouteHandler() {
214+
exists(Module mod | mod.getFile().getParentContainer() = apiFolder() |
215+
this = mod.getAnExportedValue("default").getAFunctionValue()
216+
)
217+
}
218+
219+
override Parameter getRouteHandlerParameter(string kind) {
220+
kind = "request" and result = getFunction().getParameter(0)
221+
or
222+
kind = "response" and result = getFunction().getParameter(1)
223+
}
224+
}
225+
226+
/**
227+
* Gets a reference to a [Next.js router](https://nextjs.org/docs/api-reference/next/router).
228+
*/
229+
DataFlow::SourceNode nextRouter() {
230+
result = DataFlow::moduleMember("next/router", "useRouter").getACall()
231+
or
232+
result =
233+
API::moduleImport("next/router")
234+
.getMember("withRouter")
235+
.getParameter(0)
236+
.getParameter(0)
237+
.getMember("router")
238+
.getAnImmediateUse()
239+
}
240+
}

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,18 @@ module NodeJSLib {
107107
}
108108

109109
/**
110-
* A Node.js response source, that is, the response parameter of a
110+
* A Node.js response source.
111+
*/
112+
abstract class ResponseSource extends HTTP::Servers::ResponseSource { }
113+
114+
/**
115+
* A standard Node.js response source, that is, the response parameter of a
111116
* route handler.
112117
*/
113-
private class ResponseSource extends HTTP::Servers::ResponseSource {
118+
private class StandardResponseSource extends ResponseSource {
114119
RouteHandler rh;
115120

116-
ResponseSource() { this = DataFlow::parameterNode(rh.getResponseParameter()) }
121+
StandardResponseSource() { this = DataFlow::parameterNode(rh.getResponseParameter()) }
117122

118123
/**
119124
* Gets the route handler that provides this response.
@@ -122,13 +127,18 @@ module NodeJSLib {
122127
}
123128

124129
/**
125-
* A Node.js request source, that is, the request parameter of a
130+
* A Node.js request source.
131+
*/
132+
abstract class RequestSource extends HTTP::Servers::RequestSource { }
133+
134+
/**
135+
* A standard Node.js request source, that is, the request parameter of a
126136
* route handler.
127137
*/
128-
private class RequestSource extends HTTP::Servers::RequestSource {
138+
private class StandardRequestSource extends RequestSource {
129139
RouteHandler rh;
130140

131-
RequestSource() { this = DataFlow::parameterNode(rh.getRequestParameter()) }
141+
StandardRequestSource() { this = DataFlow::parameterNode(rh.getRequestParameter()) }
132142

133143
/**
134144
* Gets the route handler that handles this request.

javascript/ql/src/semmle/javascript/security/dataflow/ClientSideUrlRedirectCustomizations.qll

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,29 @@ module ClientSideUrlRedirect {
166166
)
167167
}
168168
}
169+
170+
/**
171+
* A write to an React attribute which may execute JavaScript code.
172+
*/
173+
class ReactAttributeWriteUrlSink extends ScriptUrlSink {
174+
ReactAttributeWriteUrlSink() {
175+
exists(JSXAttribute attr |
176+
attr.getName() = DOM::getAPropertyNameInterpretedAsJavaScriptUrl() and
177+
attr.getElement().isHTMLElement()
178+
or
179+
DataFlow::moduleImport("next/link").flowsToExpr(attr.getElement().getNameExpr())
180+
|
181+
this = attr.getValue().flow()
182+
)
183+
}
184+
}
185+
186+
/**
187+
* A call to change the current url with a Next.js router.
188+
*/
189+
class NextRoutePushUrlSink extends ScriptUrlSink {
190+
NextRoutePushUrlSink() {
191+
this = NextJS::nextRouter().getAMemberCall(["push", "replace"]).getArgument(0)
192+
}
193+
}
169194
}

javascript/ql/src/semmle/javascript/security/dataflow/DOM.qll

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,7 @@ class DomPropWriteNode extends Assignment {
122122
* Holds if the assigned value is interpreted as JavaScript via javascript: protocol.
123123
*/
124124
predicate interpretsValueAsJavaScriptUrl() {
125-
lhs.getPropertyName() = "action" or
126-
lhs.getPropertyName() = "formaction" or
127-
lhs.getPropertyName() = "href" or
128-
lhs.getPropertyName() = "src" or
129-
lhs.getPropertyName() = "data"
125+
lhs.getPropertyName() = DOM::getAPropertyNameInterpretedAsJavaScriptUrl()
130126
}
131127
}
132128

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "my-app",
3+
"version": "0.1.0",
4+
"scripts": {
5+
"dev": "next dev",
6+
"build": "next build",
7+
"start": "next start"
8+
},
9+
"dependencies": {
10+
"next": "^10.0.0",
11+
"react": "17.0.1",
12+
"react-dom": "17.0.1"
13+
}
14+
}

0 commit comments

Comments
 (0)