Files
codeql/javascript/ql/lib/semmle/javascript/frameworks/WebResponse.qll
2026-01-14 12:49:41 +01:00

115 lines
3.5 KiB
Plaintext

/**
* Models the `Request` and `Response` objects from the Web standards.
*/
private import javascript
/** Treats `Response` as an entry point for API graphs. */
overlay[local?]
private class ResponseEntryPoint extends API::EntryPoint {
ResponseEntryPoint() { this = "global.Response" }
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("Response") }
}
/** Treats `Headers` as an entry point for API graphs. */
overlay[local?]
private class HeadersEntryPoint extends API::EntryPoint {
HeadersEntryPoint() { this = "global.Headers" }
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("Headers") }
}
/**
* A call to the `Response` and `NextResponse` constructor.
*/
private class ResponseCall extends API::InvokeNode {
ResponseCall() {
this = any(ResponseEntryPoint e).getANode().getAnInstantiation() or
this = API::moduleImport("next/server").getMember("NextResponse").getAnInstantiation()
}
}
/**
* A call to the `Headers` constructor.
*/
private class HeadersCall extends API::InvokeNode {
HeadersCall() { this = any(HeadersEntryPoint e).getANode().getAnInstantiation() }
}
/**
* The `headers` in `new Response(body, { headers })`
*/
private class ResponseArgumentHeaders extends Http::HeaderDefinition {
private ResponseCall response;
private API::Node headerNode;
ResponseArgumentHeaders() {
headerNode = response.getParameter(1).getMember("headers") and
this = headerNode.asSink()
or
not exists(response.getParameter(1).getMember("headers")) and
headerNode = API::root() and // just bind 'headerNode' to something
this = response
}
ResponseCall getResponse() { result = response }
/**
* Gets a call to `new Headers()` that is passed as the headers to this call.
*/
private HeadersCall getHeadersCall() { headerNode.refersTo(result.getReturn()) }
/**
* Gets an object whose properties are interpreted as headers, such as `{'content-type': 'foo'}`.
*/
private API::Node getAPlainHeaderObject() {
// new Response(body, {...})
result = headerNode
or
// new Response(body, new Headers({...}))
result = this.getHeadersCall().getParameter(0)
}
private API::Node getHeaderNode(string headerName) {
exists(string prop |
result = this.getAPlainHeaderObject().getMember(prop) and
headerName = prop.toLowerCase()
)
or
exists(API::CallNode append |
append = this.getHeadersCall().getReturn().getMember(["append", "set"]).getACall() and
headerName = append.getArgument(0).getStringValue().toLowerCase() and
result = append.getParameter(1)
)
}
override predicate defines(string headerName, string headerValue) {
this.getHeaderNode(headerName).getAValueReachingSink().getStringValue() = headerValue
or
// If no 'content-type' header is defined, a default one is sent in the HTTP response.
not exists(this.getHeaderNode("content-type")) and
headerName = "content-type" and
headerValue = "text/plain;charset=utf-8"
}
override string getAHeaderName() { exists(this.getHeaderNode(result)) or result = "content-type" }
override Http::RouteHandler getRouteHandler() { none() }
}
/**
* Data passed as the body in `new Response(body, ...)`.
*/
private class ResponseSink extends Http::ResponseSendArgument {
private ResponseCall response;
ResponseSink() { this = response.getArgument(0) }
override Http::RouteHandler getRouteHandler() { none() }
override ResponseArgumentHeaders getAnAssociatedHeaderDefinition() {
result.getResponse() = response
}
}