Merge pull request #20916 from asgerf/js/next-folders2

JS: Handle Next.js files named 'page' or 'route'
This commit is contained in:
Asger F
2026-01-14 11:10:57 +01:00
committed by GitHub
6 changed files with 68 additions and 20 deletions

View File

@@ -13,18 +13,9 @@ module NextJS {
*/
PackageJson getANextPackage() { result.getDependencies().getADependency("next", _) }
bindingset[base, name]
pragma[inline_late]
private Folder getOptionalFolder(Folder base, string name) {
result = base.getFolder(name)
or
not exists(base.getFolder(name)) and
result = base
}
private Folder packageRoot() { result = getANextPackage().getFile().getParentContainer() }
private Folder srcRoot() { result = getOptionalFolder(packageRoot(), "src") }
private Folder srcRoot() { result = [packageRoot(), packageRoot().getFolder("src")] }
private Folder appRoot() { result = srcRoot().getFolder("app") }
@@ -32,20 +23,30 @@ module NextJS {
private Folder apiRoot() { result = [pagesRoot(), appRoot()].getFolder("api") }
private Folder appFolder() {
result = appRoot()
or
result = appFolder().getAFolder()
}
private Folder pagesFolder() {
result = pagesRoot()
or
result = pagesFolder().getAFolder()
}
/**
* Gets a "pages" folder in a `Next.js` application.
* JavaScript files inside these folders are mapped to routes.
*/
Folder getAPagesFolder() {
result = pagesRoot()
or
result = getAPagesFolder().getAFolder()
}
deprecated predicate getAPagesFolder = pagesFolder/0;
/**
* Gets a module corrosponding to a `Next.js` page.
* Gets a module corresponding to a `Next.js` page.
*/
Module getAPagesModule() { result.getFile().getParentContainer() = getAPagesFolder() }
Module getAPagesModule() {
result.getFile() = [pagesFolder().getAFile(), appFolder().getJavaScriptFile("page")]
}
/**
* Gets a module inside a "pages" folder where `fallback` from `getStaticPaths` is not set to false.
@@ -300,11 +301,17 @@ module NextJS {
class NextAppRouteHandler extends DataFlow::FunctionNode, Http::Servers::StandardRouteHandler {
NextAppRouteHandler() {
exists(Module mod |
mod.getFile().getParentContainer() = apiFolder() or
mod.getFile().getStem() = "middleware"
(
mod.getFile().getParentContainer() = apiFolder()
or
mod.getFile().getStem() = "middleware"
or
mod.getFile().getStem() = "route" and mod.getFile().getParentContainer() = appFolder()
)
|
this =
mod.getAnExportedValue([any(Http::RequestMethodName m), "middleware"]).getAFunctionValue()
mod.getAnExportedValue([any(Http::RequestMethodName m), "middleware", "proxy"])
.getAFunctionValue()
)
}

View File

@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* Fixed a bug in the Next.js model that would cause the analysis to miss server-side taint sources in files
named `route` or `page` appearing outside `api` and `pages` folders.

View File

@@ -35,6 +35,9 @@
| app/api/routeNextRequest.ts:15:20:15:23 | body | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | app/api/routeNextRequest.ts:15:20:15:23 | body | Cross-site scripting vulnerability due to a $@. | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | user-provided value |
| app/api/routeNextRequest.ts:27:20:27:23 | body | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | app/api/routeNextRequest.ts:27:20:27:23 | body | Cross-site scripting vulnerability due to a $@. | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | user-provided value |
| app/api/routeNextRequest.ts:31:27:31:30 | body | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | app/api/routeNextRequest.ts:31:27:31:30 | body | Cross-site scripting vulnerability due to a $@. | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | user-provided value |
| app/blah/page.jsx:8:13:8:19 | req.url | app/blah/page.jsx:8:13:8:19 | req.url | app/blah/page.jsx:8:13:8:19 | req.url | Cross-site scripting vulnerability due to a $@. | app/blah/page.jsx:8:13:8:19 | req.url | user-provided value |
| app/blah/page.jsx:15:13:15:19 | req.url | app/blah/page.jsx:15:13:15:19 | req.url | app/blah/page.jsx:15:13:15:19 | req.url | Cross-site scripting vulnerability due to a $@. | app/blah/page.jsx:15:13:15:19 | req.url | user-provided value |
| app/blah/route.ts:3:25:3:27 | url | app/blah/route.ts:2:17:2:23 | req.url | app/blah/route.ts:3:25:3:27 | url | Cross-site scripting vulnerability due to a $@. | app/blah/route.ts:2:17:2:23 | req.url | user-provided value |
| app/pages/Next2.jsx:8:13:8:19 | req.url | app/pages/Next2.jsx:8:13:8:19 | req.url | app/pages/Next2.jsx:8:13:8:19 | req.url | Cross-site scripting vulnerability due to a $@. | app/pages/Next2.jsx:8:13:8:19 | req.url | user-provided value |
| app/pages/Next2.jsx:15:13:15:19 | req.url | app/pages/Next2.jsx:15:13:15:19 | req.url | app/pages/Next2.jsx:15:13:15:19 | req.url | Cross-site scripting vulnerability due to a $@. | app/pages/Next2.jsx:15:13:15:19 | req.url | user-provided value |
| etherpad.js:11:12:11:19 | response | etherpad.js:9:16:9:30 | req.query.jsonp | etherpad.js:11:12:11:19 | response | Cross-site scripting vulnerability due to a $@. | etherpad.js:9:16:9:30 | req.query.jsonp | user-provided value |
@@ -149,6 +152,8 @@ edges
| app/api/routeNextRequest.ts:4:9:4:12 | body | app/api/routeNextRequest.ts:31:27:31:30 | body | provenance | |
| app/api/routeNextRequest.ts:4:16:4:31 | await req.json() | app/api/routeNextRequest.ts:4:9:4:12 | body | provenance | |
| app/api/routeNextRequest.ts:4:22:4:31 | req.json() | app/api/routeNextRequest.ts:4:16:4:31 | await req.json() | provenance | |
| app/blah/route.ts:2:11:2:13 | url | app/blah/route.ts:3:25:3:27 | url | provenance | |
| app/blah/route.ts:2:17:2:23 | req.url | app/blah/route.ts:2:11:2:13 | url | provenance | |
| etherpad.js:9:5:9:12 | response | etherpad.js:11:12:11:19 | response | provenance | |
| etherpad.js:9:16:9:30 | req.query.jsonp | etherpad.js:9:5:9:12 | response | provenance | |
| formatting.js:4:9:4:12 | evil | formatting.js:6:43:6:46 | evil | provenance | |
@@ -357,6 +362,11 @@ nodes
| app/api/routeNextRequest.ts:15:20:15:23 | body | semmle.label | body |
| app/api/routeNextRequest.ts:27:20:27:23 | body | semmle.label | body |
| app/api/routeNextRequest.ts:31:27:31:30 | body | semmle.label | body |
| app/blah/page.jsx:8:13:8:19 | req.url | semmle.label | req.url |
| app/blah/page.jsx:15:13:15:19 | req.url | semmle.label | req.url |
| app/blah/route.ts:2:11:2:13 | url | semmle.label | url |
| app/blah/route.ts:2:17:2:23 | req.url | semmle.label | req.url |
| app/blah/route.ts:3:25:3:27 | url | semmle.label | url |
| app/pages/Next2.jsx:8:13:8:19 | req.url | semmle.label | req.url |
| app/pages/Next2.jsx:15:13:15:19 | req.url | semmle.label | req.url |
| etherpad.js:9:5:9:12 | response | semmle.label | response |

View File

@@ -34,6 +34,9 @@
| app/api/routeNextRequest.ts:15:20:15:23 | body | Cross-site scripting vulnerability due to $@. | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | user-provided value |
| app/api/routeNextRequest.ts:27:20:27:23 | body | Cross-site scripting vulnerability due to $@. | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | user-provided value |
| app/api/routeNextRequest.ts:31:27:31:30 | body | Cross-site scripting vulnerability due to $@. | app/api/routeNextRequest.ts:4:22:4:31 | req.json() | user-provided value |
| app/blah/page.jsx:8:13:8:19 | req.url | Cross-site scripting vulnerability due to $@. | app/blah/page.jsx:8:13:8:19 | req.url | user-provided value |
| app/blah/page.jsx:15:13:15:19 | req.url | Cross-site scripting vulnerability due to $@. | app/blah/page.jsx:15:13:15:19 | req.url | user-provided value |
| app/blah/route.ts:3:25:3:27 | url | Cross-site scripting vulnerability due to $@. | app/blah/route.ts:2:17:2:23 | req.url | user-provided value |
| app/pages/Next2.jsx:8:13:8:19 | req.url | Cross-site scripting vulnerability due to $@. | app/pages/Next2.jsx:8:13:8:19 | req.url | user-provided value |
| app/pages/Next2.jsx:15:13:15:19 | req.url | Cross-site scripting vulnerability due to $@. | app/pages/Next2.jsx:15:13:15:19 | req.url | user-provided value |
| formatting.js:6:14:6:47 | util.fo ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |

View File

@@ -0,0 +1,19 @@
export default function Page() {
return <span />;
}
Page.getInitialProps = async (ctx) => {
const req = ctx.req;
const res = ctx.res;
res.end(req.url); // $ Alert
return {}
}
export async function getServerSideProps(ctx) {
const req = ctx.req;
const res = ctx.res;
res.end(req.url); // $ Alert
return {
props: {}
}
}

View File

@@ -0,0 +1,4 @@
export async function GET(req: Request) {
const url = req.url; // $ Source
return new Response(url, { headers: { "Content-Type": "text/html" } }); // $ Alert
}