Merge pull request #5183 from erik-krogh/next

Approved by asgerf
This commit is contained in:
CodeQL CI
2021-03-04 04:57:43 -08:00
committed by GitHub
20 changed files with 511 additions and 11 deletions

View File

@@ -97,6 +97,7 @@ import semmle.javascript.frameworks.Logging
import semmle.javascript.frameworks.HttpFrameworks
import semmle.javascript.frameworks.HttpProxy
import semmle.javascript.frameworks.Markdown
import semmle.javascript.frameworks.Next
import semmle.javascript.frameworks.NoSQL
import semmle.javascript.frameworks.PkgCloud
import semmle.javascript.frameworks.PropertyProjection

View File

@@ -491,4 +491,11 @@ module DOM {
or
result.hasUnderlyingType("Document")
}
/**
* Holds if a value assigned to property `name` of a DOM node can be interpreted as JavaScript via the `javascript:` protocol.
*/
string getAPropertyNameInterpretedAsJavaScriptUrl() {
result = ["action", "formaction", "href", "src", "data"]
}
}

View File

@@ -65,6 +65,12 @@ class JSXElement extends JSXNode {
}
override string getAPrimaryQlClass() { result = "JSXElement" }
/**
* Holds if this JSX element is a HTML element.
* That is, the name starts with a lowercase letter.
*/
predicate isHTMLElement() { getName().regexpMatch("[a-z].*") }
}
/**

View File

@@ -0,0 +1,240 @@
/**
* Provides classes and predicates for reasoning about [Next.js](https://www.npmjs.com/package/next).
*/
import javascript
/**
* Provides classes and predicates modelling [Next.js](https://www.npmjs.com/package/next).
*/
module NextJS {
/**
* Gets a `package.json` that depends on the `Next.js` library.
*/
PackageJSON getANextPackage() { result.getDependencies().getADependency("next", _) }
/**
* Gets a "pages" folder in a `Next.js` application.
* JavaScript files inside these folders are mapped to routes.
*/
Folder getAPagesFolder() {
result = getANextPackage().getFile().getParentContainer().getFolder("pages")
or
result = getAPagesFolder().getAFolder()
}
/**
* Gets a module corrosponding to a `Next.js` page.
*/
Module getAPagesModule() { result.getFile().getParentContainer() = getAPagesFolder() }
/**
* Gets a module inside a "pages" folder where `fallback` from `getStaticPaths` is not set to false.
* In such a module the `getStaticProps` method can be called with user-defined parameters.
* If `fallback` is set to false, then only values defined by `getStaticPaths` are allowed.
*/
Module getAModuleWithFallbackPaths() {
result = getAPagesModule() and
exists(DataFlow::FunctionNode staticPaths, Expr fallback |
staticPaths = result.getAnExportedValue("getStaticPaths").getAFunctionValue() and
fallback =
staticPaths.getAReturn().getALocalSource().getAPropertyWrite("fallback").getRhs().asExpr() and
not fallback.(BooleanLiteral).getValue() = "false"
)
}
/**
* User defined path parameter in `Next.js`.
*/
class NextParams extends RemoteFlowSource {
NextParams() {
this =
getAModuleWithFallbackPaths()
.getAnExportedValue("getStaticProps")
.getAFunctionValue()
.getParameter(0)
.getAPropertyRead("params")
}
override string getSourceType() { result = "Next request parameter" }
}
/**
* Gets the `getStaticProps` function in a Next.js page.
* 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).
*/
DataFlow::FunctionNode getStaticPropsFunction(Module pageModule) {
pageModule = getAPagesModule() and
result = pageModule.getAnExportedValue("getStaticProps").getAFunctionValue()
}
/**
* Gets the `getServerSideProps` function in a Next.js page.
* This function is executed on the server every time a request for the page is made.
* The function receives a context parameter, which includes HTTP request/response objects.
*/
DataFlow::FunctionNode getServerSidePropsFunction(Module pageModule) {
pageModule = getAPagesModule() and
result = pageModule.getAnExportedValue("getServerSideProps").getAFunctionValue()
}
/**
* Gets the `getInitialProps` function in a Next.js page.
* This function is executed on the server every time a request for the page is made.
* The function receives a context parameter, which includes HTTP request/response objects.
*/
DataFlow::FunctionNode getInitialProps(Module pageModule) {
pageModule = getAPagesModule() and
(
result =
pageModule
.getAnExportedValue("default")
.getAFunctionValue()
.getAPropertyWrite("getInitialProps")
.getRhs()
.getAFunctionValue()
or
result =
pageModule
.getAnExportedValue("default")
.getALocalSource()
.getAstNode()
.(ReactComponent)
.getStaticMethod("getInitialProps")
.flow()
)
}
/**
* Gets a reference to a `props` object computed by the Next.js server.
* This `props` object is both used both by the server and client to render the page.
*/
DataFlow::Node getAPropsSource(Module pageModule) {
pageModule = getAPagesModule() and
(
result =
[getStaticPropsFunction(pageModule), getServerSidePropsFunction(pageModule)]
.getAReturn()
.getALocalSource()
.getAPropertyWrite("props")
.getRhs()
or
result = getInitialProps(pageModule).getAReturn()
)
}
/**
* A step modelling the flow from the server-computed props object to the default exported function that renders the page.
*/
class NextJSStaticPropsStep extends DataFlow::AdditionalFlowStep, DataFlow::FunctionNode {
Module pageModule;
NextJSStaticPropsStep() {
pageModule = getAPagesModule() and
this = pageModule.getAnExportedValue("default").getAFunctionValue()
}
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
pred = getAPropsSource(pageModule) and
succ = this.getParameter(0)
}
}
/**
* A step modelling the flow from the server-computed props object to the default exported React component that renders the page.
*/
class NextJSStaticReactComponentPropsStep extends DataFlow::AdditionalFlowStep,
DataFlow::ValueNode {
Module pageModule;
ReactComponent component;
NextJSStaticReactComponentPropsStep() {
pageModule = getAPagesModule() and
this.getAstNode() = component and
this = pageModule.getAnExportedValue("default").getALocalSource()
}
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
pred = getAPropsSource(pageModule) and
succ = component.getADirectPropsAccess()
}
}
/**
* A Next.js function that is exected on the server for every request, seen as a routehandler.
*/
class NextHttpRouteHandler extends HTTP::Servers::StandardRouteHandler, DataFlow::FunctionNode {
Module pageModule;
NextHttpRouteHandler() {
this = getServerSidePropsFunction(pageModule) or this = getInitialProps(pageModule)
}
}
/**
* A NodeJS HTTP request object in a Next.js page.
*/
class NextHttpRequestSource extends NodeJSLib::RequestSource {
NextHttpRouteHandler rh;
NextHttpRequestSource() { this = rh.getParameter(0).getAPropertyRead("req") }
override HTTP::RouteHandler getRouteHandler() { result = rh }
}
/**
* A NodeJS HTTP response object in a Next.js page.
*/
class NextHttpResponseSource extends NodeJSLib::ResponseSource {
NextHttpRouteHandler rh;
NextHttpResponseSource() { this = rh.getParameter(0).getAPropertyRead("res") }
override HTTP::RouteHandler getRouteHandler() { result = rh }
}
/**
* Gets a folder that contains API endpoints for a Next.js application.
* These API endpoints act as Express-like route-handlers.
*/
Folder apiFolder() {
result = getANextPackage().getFile().getParentContainer().getFolder("pages").getFolder("api")
or
result = apiFolder().getAFolder()
}
/**
* A Next.js route handler for an API endpoint.
* The response (res) includes a set of Express.js-like methods,
* and we therefore model the routehandler as an Express.js routehandler.
*/
class NextAPIRouteHandler extends DataFlow::FunctionNode, Express::RouteHandler,
HTTP::Servers::StandardRouteHandler {
NextAPIRouteHandler() {
exists(Module mod | mod.getFile().getParentContainer() = apiFolder() |
this = mod.getAnExportedValue("default").getAFunctionValue()
)
}
override Parameter getRouteHandlerParameter(string kind) {
kind = "request" and result = getFunction().getParameter(0)
or
kind = "response" and result = getFunction().getParameter(1)
}
}
/**
* Gets a reference to a [Next.js router](https://nextjs.org/docs/api-reference/next/router).
*/
DataFlow::SourceNode nextRouter() {
result = DataFlow::moduleMember("next/router", "useRouter").getACall()
or
result =
API::moduleImport("next/router")
.getMember("withRouter")
.getParameter(0)
.getParameter(0)
.getMember("router")
.getAnImmediateUse()
}
}

View File

@@ -107,13 +107,18 @@ module NodeJSLib {
}
/**
* A Node.js response source, that is, the response parameter of a
* A Node.js response source.
*/
abstract class ResponseSource extends HTTP::Servers::ResponseSource { }
/**
* A standard Node.js response source, that is, the response parameter of a
* route handler.
*/
private class ResponseSource extends HTTP::Servers::ResponseSource {
private class StandardResponseSource extends ResponseSource {
RouteHandler rh;
ResponseSource() { this = DataFlow::parameterNode(rh.getResponseParameter()) }
StandardResponseSource() { this = DataFlow::parameterNode(rh.getResponseParameter()) }
/**
* Gets the route handler that provides this response.
@@ -122,13 +127,18 @@ module NodeJSLib {
}
/**
* A Node.js request source, that is, the request parameter of a
* A Node.js request source.
*/
abstract class RequestSource extends HTTP::Servers::RequestSource { }
/**
* A standard Node.js request source, that is, the request parameter of a
* route handler.
*/
private class RequestSource extends HTTP::Servers::RequestSource {
private class StandardRequestSource extends RequestSource {
RouteHandler rh;
RequestSource() { this = DataFlow::parameterNode(rh.getRequestParameter()) }
StandardRequestSource() { this = DataFlow::parameterNode(rh.getRequestParameter()) }
/**
* Gets the route handler that handles this request.

View File

@@ -166,4 +166,29 @@ module ClientSideUrlRedirect {
)
}
}
/**
* A write to an React attribute which may execute JavaScript code.
*/
class ReactAttributeWriteUrlSink extends ScriptUrlSink {
ReactAttributeWriteUrlSink() {
exists(JSXAttribute attr |
attr.getName() = DOM::getAPropertyNameInterpretedAsJavaScriptUrl() and
attr.getElement().isHTMLElement()
or
DataFlow::moduleImport("next/link").flowsToExpr(attr.getElement().getNameExpr())
|
this = attr.getValue().flow()
)
}
}
/**
* A call to change the current url with a Next.js router.
*/
class NextRoutePushUrlSink extends ScriptUrlSink {
NextRoutePushUrlSink() {
this = NextJS::nextRouter().getAMemberCall(["push", "replace"]).getArgument(0)
}
}
}

View File

@@ -122,11 +122,7 @@ class DomPropWriteNode extends Assignment {
* Holds if the assigned value is interpreted as JavaScript via javascript: protocol.
*/
predicate interpretsValueAsJavaScriptUrl() {
lhs.getPropertyName() = "action" or
lhs.getPropertyName() = "formaction" or
lhs.getPropertyName() = "href" or
lhs.getPropertyName() = "src" or
lhs.getPropertyName() = "data"
lhs.getPropertyName() = DOM::getAPropertyNameInterpretedAsJavaScriptUrl()
}
}