mirror of
https://github.com/github/codeql.git
synced 2025-12-17 01:03:14 +01:00
@@ -129,6 +129,7 @@ JavaScript and TypeScript built-in support
|
||||
mssql, Database
|
||||
mysql, Database
|
||||
node, Runtime environment
|
||||
nest.js, Server
|
||||
postgres, Database
|
||||
ramda, Utility library
|
||||
react, HTML framework
|
||||
|
||||
3
javascript/change-notes/2021-04-15-fs-promises.md
Normal file
3
javascript/change-notes/2021-04-15-fs-promises.md
Normal file
@@ -0,0 +1,3 @@
|
||||
lgtm,codescanning
|
||||
* Support for `fs.promises` has been added, leading to more results for security queries
|
||||
related to file system access.
|
||||
3
javascript/change-notes/2021-04-15-nestjs.md
Normal file
3
javascript/change-notes/2021-04-15-nestjs.md
Normal file
@@ -0,0 +1,3 @@
|
||||
lgtm,codescanning
|
||||
* Support for Nest.js has been added. The security queries now recognize sources and sinks
|
||||
specific to the Nest.js framework.
|
||||
@@ -76,6 +76,7 @@ import semmle.javascript.frameworks.Babel
|
||||
import semmle.javascript.frameworks.Cheerio
|
||||
import semmle.javascript.frameworks.ComposedFunctions
|
||||
import semmle.javascript.frameworks.Classnames
|
||||
import semmle.javascript.frameworks.ClassValidator
|
||||
import semmle.javascript.frameworks.ClientRequests
|
||||
import semmle.javascript.frameworks.ClosureLibrary
|
||||
import semmle.javascript.frameworks.CookieLibraries
|
||||
@@ -99,6 +100,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.Nest
|
||||
import semmle.javascript.frameworks.Next
|
||||
import semmle.javascript.frameworks.NoSQL
|
||||
import semmle.javascript.frameworks.PkgCloud
|
||||
|
||||
@@ -47,6 +47,9 @@ class ParameterNode extends DataFlow::SourceNode {
|
||||
|
||||
/** Holds if this parameter is a rest parameter. */
|
||||
predicate isRestParameter() { p.isRestParameter() }
|
||||
|
||||
/** Gets the data flow node for an expression that is applied to this decorator. */
|
||||
DataFlow::Node getADecorator() { result = getParameter().getADecorator().getExpression().flow() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1078,35 +1081,42 @@ module ClassNode {
|
||||
* Subclass this to introduce new kinds of class nodes. If you want to refine
|
||||
* the definition of existing class nodes, subclass `DataFlow::ClassNode` instead.
|
||||
*/
|
||||
cached
|
||||
abstract class Range extends DataFlow::SourceNode {
|
||||
/**
|
||||
* Gets the name of the class, if it has one.
|
||||
*/
|
||||
cached
|
||||
abstract string getName();
|
||||
|
||||
/**
|
||||
* Gets a description of the class.
|
||||
*/
|
||||
cached
|
||||
abstract string describe();
|
||||
|
||||
/**
|
||||
* Gets the constructor function of this class.
|
||||
*/
|
||||
cached
|
||||
abstract FunctionNode getConstructor();
|
||||
|
||||
/**
|
||||
* Gets the instance member with the given name and kind.
|
||||
*/
|
||||
cached
|
||||
abstract FunctionNode getInstanceMember(string name, MemberKind kind);
|
||||
|
||||
/**
|
||||
* Gets an instance member with the given kind.
|
||||
*/
|
||||
cached
|
||||
abstract FunctionNode getAnInstanceMember(MemberKind kind);
|
||||
|
||||
/**
|
||||
* Gets the static method of this class with the given name.
|
||||
*/
|
||||
cached
|
||||
abstract FunctionNode getStaticMethod(string name);
|
||||
|
||||
/**
|
||||
@@ -1114,20 +1124,24 @@ module ClassNode {
|
||||
*
|
||||
* The constructor is not considered a static method.
|
||||
*/
|
||||
cached
|
||||
abstract FunctionNode getAStaticMethod();
|
||||
|
||||
/**
|
||||
* Gets a dataflow node representing a class to be used as the super-class
|
||||
* of this node.
|
||||
*/
|
||||
cached
|
||||
abstract DataFlow::Node getASuperClassNode();
|
||||
|
||||
/**
|
||||
* Gets the type annotation for the field `fieldName`, if any.
|
||||
*/
|
||||
cached
|
||||
TypeAnnotation getFieldTypeAnnotation(string fieldName) { none() }
|
||||
|
||||
/** Gets a decorator applied to this class. */
|
||||
cached
|
||||
DataFlow::Node getADecorator() { none() }
|
||||
}
|
||||
|
||||
|
||||
@@ -550,7 +550,8 @@ module TaintTracking {
|
||||
// reading from a tainted object yields a tainted result
|
||||
succ.(DataFlow::PropRead).getBase() = pred and
|
||||
not AccessPath::DominatingPaths::hasDominatingWrite(succ) and
|
||||
not isSafeClientSideUrlProperty(succ)
|
||||
not isSafeClientSideUrlProperty(succ) and
|
||||
not ClassValidator::isAccessToSanitizedField(succ)
|
||||
or
|
||||
// iterating over a tainted iterator taints the loop variable
|
||||
exists(ForOfStmt fos |
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Provides predicates for reasoning about sanitization via the `class-validator` library.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Provides predicates for reasoning about sanitization via the `class-validator` library.
|
||||
*/
|
||||
module ClassValidator {
|
||||
/**
|
||||
* Holds if the decorator with the given name sanitizes the input, for the purpose of taint tracking.
|
||||
*/
|
||||
bindingset[name]
|
||||
private predicate isSanitizingDecoratorName(string name) {
|
||||
// Most decorators do sanitize the input, so only list those that don't.
|
||||
not name =
|
||||
[
|
||||
"IsDefined", "IsOptional", "NotEquals", "IsNotEmpty", "IsNotIn", "IsString", "IsArray",
|
||||
"Contains", "NotContains", "IsAscii", "IsByteLength", "IsDataURI", "IsFQDN", "IsJSON",
|
||||
"IsJWT", "IsObject", "IsNotEmptyObject", "IsLowercase", "IsSurrogatePair", "IsUrl",
|
||||
"IsUppercase", "Length", "MinLength", "MaxLength", "ArrayContains", "ArrayNotContains",
|
||||
"ArrayNotEmpty", "ArrayMinSize", "ArrayMaxSize", "ArrayUnique", "Allow", "ValidateNested",
|
||||
"Validate",
|
||||
// Consider "Matches" to be non-sanitizing as it is special-cased below
|
||||
"Matches"
|
||||
]
|
||||
}
|
||||
|
||||
/** Holds if the given call is a decorator that sanitizes values for the purpose of taint tracking, such as `IsBoolean()`. */
|
||||
API::CallNode sanitizingDecorator() {
|
||||
exists(string name | result = API::moduleImport("class-validator").getMember(name).getACall() |
|
||||
isSanitizingDecoratorName(name)
|
||||
or
|
||||
name = "Matches" and
|
||||
RegExp::isGenericRegExpSanitizer(RegExp::getRegExpFromNode(result.getArgument(0)), true)
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if the given field has a decorator that sanitizes its value for the purpose of taint tracking. */
|
||||
predicate isFieldSanitizedByDecorator(FieldDefinition field) {
|
||||
field.getADecorator().getExpression().flow() = sanitizingDecorator().getReturn().getAUse()
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate isFieldSanitizedByDecorator(ClassDefinition cls, string name) {
|
||||
isFieldSanitizedByDecorator(cls.getField(name))
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private ClassDefinition getClassReferencedByPropRead(DataFlow::PropRead read) {
|
||||
read.getBase().asExpr().getType().unfold().(ClassType).getClass() = result
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the given property read refers to a field that has a sanitizing decorator.
|
||||
*
|
||||
* Only holds when TypeScript types are available.
|
||||
*/
|
||||
pragma[noinline]
|
||||
predicate isAccessToSanitizedField(DataFlow::PropRead read) {
|
||||
exists(ClassDefinition class_ |
|
||||
class_ = getClassReferencedByPropRead(read) and
|
||||
isFieldSanitizedByDecorator(class_, read.getPropertyName())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -396,7 +396,7 @@ module Express {
|
||||
}
|
||||
|
||||
/** An Express response source. */
|
||||
abstract private class ResponseSource extends HTTP::Servers::ResponseSource { }
|
||||
abstract class ResponseSource extends HTTP::Servers::ResponseSource { }
|
||||
|
||||
/**
|
||||
* An Express response source, that is, the response parameter of a
|
||||
@@ -427,7 +427,7 @@ module Express {
|
||||
}
|
||||
|
||||
/** An Express request source. */
|
||||
abstract private class RequestSource extends HTTP::Servers::RequestSource { }
|
||||
abstract class RequestSource extends HTTP::Servers::RequestSource { }
|
||||
|
||||
/**
|
||||
* An Express request source, that is, the request parameter of a
|
||||
@@ -471,73 +471,98 @@ module Express {
|
||||
* Gets a reference to the "query" object from a request-object originating from route-handler `rh`.
|
||||
*/
|
||||
DataFlow::SourceNode getAQueryObjectReference(DataFlow::TypeTracker t, RouteHandler rh) {
|
||||
t.startInProp("query") and
|
||||
result = rh.getARequestSource()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = getAQueryObjectReference(t2, rh).track(t2, t))
|
||||
result = queryRef(rh.getARequestSource(), t)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to the "params" object from a request-object originating from route-handler `rh`.
|
||||
*/
|
||||
DataFlow::SourceNode getAParamsObjectReference(DataFlow::TypeTracker t, RouteHandler rh) {
|
||||
t.startInProp("params") and
|
||||
result = rh.getARequestSource()
|
||||
result = paramsRef(rh.getARequestSource(), t)
|
||||
}
|
||||
|
||||
/** The input parameter to an `app.param()` route handler. */
|
||||
private class ParamHandlerInputAccess extends HTTP::RequestInputAccess {
|
||||
RouteHandler rh;
|
||||
|
||||
ParamHandlerInputAccess() {
|
||||
exists(RouteSetup setup | rh = setup.getARouteHandler() |
|
||||
this = DataFlow::parameterNode(rh.getRouteHandlerParameter("parameter"))
|
||||
)
|
||||
}
|
||||
|
||||
override HTTP::RouteHandler getRouteHandler() { result = rh }
|
||||
|
||||
override string getKind() { result = "parameter" }
|
||||
}
|
||||
|
||||
/** Gets a data flow node referring to `req.query`. */
|
||||
private DataFlow::SourceNode queryRef(RequestSource req, DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result = req.ref().getAPropertyRead("query")
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = getAParamsObjectReference(t2, rh).track(t2, t))
|
||||
exists(DataFlow::TypeTracker t2 | result = queryRef(req, t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a data flow node referring to `req.query`. */
|
||||
private DataFlow::SourceNode queryRef(RequestSource req) {
|
||||
result = queryRef(req, DataFlow::TypeTracker::end())
|
||||
}
|
||||
|
||||
/** Gets a data flow node referring to `req.params`. */
|
||||
private DataFlow::SourceNode paramsRef(RequestSource req, DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result = req.ref().getAPropertyRead("params")
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = paramsRef(req, t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a data flow node referring to `req.params`. */
|
||||
private DataFlow::SourceNode paramsRef(RequestSource req) {
|
||||
result = paramsRef(req, DataFlow::TypeTracker::end())
|
||||
}
|
||||
|
||||
/**
|
||||
* An access to a user-controlled Express request input.
|
||||
*/
|
||||
class RequestInputAccess extends HTTP::RequestInputAccess {
|
||||
RouteHandler rh;
|
||||
RequestSource request;
|
||||
string kind;
|
||||
|
||||
RequestInputAccess() {
|
||||
kind = "parameter" and
|
||||
this =
|
||||
[
|
||||
getAQueryObjectReference(DataFlow::TypeTracker::end(), rh),
|
||||
getAParamsObjectReference(DataFlow::TypeTracker::end(), rh)
|
||||
].getAPropertyRead()
|
||||
this = [queryRef(request), paramsRef(request)].getAPropertyRead()
|
||||
or
|
||||
exists(DataFlow::SourceNode request | request = rh.getARequestSource().ref() |
|
||||
exists(DataFlow::SourceNode ref | ref = request.ref() |
|
||||
kind = "parameter" and
|
||||
this = request.getAMethodCall("param")
|
||||
this = ref.getAMethodCall("param")
|
||||
or
|
||||
// `req.originalUrl`
|
||||
kind = "url" and
|
||||
this = request.getAPropertyRead("originalUrl")
|
||||
this = ref.getAPropertyRead("originalUrl")
|
||||
or
|
||||
// `req.cookies`
|
||||
kind = "cookie" and
|
||||
this = request.getAPropertyRead("cookies")
|
||||
this = ref.getAPropertyRead("cookies")
|
||||
or
|
||||
// `req.files`, treated the same as `req.body`.
|
||||
// `express-fileupload` uses .files, and `multer` uses .files or .file
|
||||
kind = "body" and
|
||||
this = request.getAPropertyRead(["files", "file"])
|
||||
)
|
||||
or
|
||||
kind = "body" and
|
||||
this.asExpr() = rh.getARequestBodyAccess()
|
||||
or
|
||||
// `value` in `router.param('foo', (req, res, next, value) => { ... })`
|
||||
kind = "parameter" and
|
||||
exists(RouteSetup setup | rh = setup.getARouteHandler() |
|
||||
this = DataFlow::parameterNode(rh.getRouteHandlerParameter("parameter"))
|
||||
this = ref.getAPropertyRead(["files", "file"])
|
||||
or
|
||||
kind = "body" and
|
||||
this = ref.getAPropertyRead("body")
|
||||
)
|
||||
}
|
||||
|
||||
override RouteHandler getRouteHandler() { result = rh }
|
||||
override RouteHandler getRouteHandler() { result = request.getRouteHandler() }
|
||||
|
||||
override string getKind() { result = kind }
|
||||
|
||||
override predicate isUserControlledObject() {
|
||||
kind = "body" and
|
||||
exists(ExpressLibraries::BodyParser bodyParser, RouteHandlerExpr expr |
|
||||
expr.getBody() = rh and
|
||||
expr.getBody() = request.getRouteHandler() and
|
||||
bodyParser.producesUserControlledObjects() and
|
||||
bodyParser.flowsToExpr(expr.getAMatchingAncestor())
|
||||
)
|
||||
@@ -548,13 +573,11 @@ module Express {
|
||||
forall(ExpressLibraries::BodyParser bodyParser | bodyParser.producesUserControlledObjects())
|
||||
or
|
||||
kind = "parameter" and
|
||||
exists(DataFlow::Node request | request = DataFlow::valueNode(rh.getARequestExpr()) |
|
||||
this.(DataFlow::MethodCallNode).calls(request, "param")
|
||||
)
|
||||
this = request.ref().getAMethodCall("param")
|
||||
or
|
||||
// `req.query.name`
|
||||
kind = "parameter" and
|
||||
this = getAQueryObjectReference(DataFlow::TypeTracker::end(), rh).getAPropertyRead()
|
||||
this = queryRef(request).getAPropertyRead()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,29 +585,14 @@ module Express {
|
||||
* An access to a header on an Express request.
|
||||
*/
|
||||
private class RequestHeaderAccess extends HTTP::RequestHeaderAccess {
|
||||
RouteHandler rh;
|
||||
RequestSource request;
|
||||
|
||||
RequestHeaderAccess() {
|
||||
exists(DataFlow::Node request | request = DataFlow::valueNode(rh.getARequestExpr()) |
|
||||
exists(string methodName |
|
||||
// `req.get(...)` or `req.header(...)`
|
||||
this.(DataFlow::MethodCallNode).calls(request, methodName)
|
||||
|
|
||||
methodName = "get" or
|
||||
methodName = "header"
|
||||
)
|
||||
or
|
||||
exists(DataFlow::PropRead headers |
|
||||
// `req.headers.name`
|
||||
headers.accesses(request, "headers") and
|
||||
this = headers.getAPropertyRead()
|
||||
)
|
||||
or
|
||||
exists(string propName | propName = "host" or propName = "hostname" |
|
||||
// `req.host` and `req.hostname` are derived from headers
|
||||
this.(DataFlow::PropRead).accesses(request, propName)
|
||||
)
|
||||
)
|
||||
this = request.ref().getAMethodCall(["get", "header"])
|
||||
or
|
||||
this = request.ref().getAPropertyRead("headers").getAPropertyRead()
|
||||
or
|
||||
this = request.ref().getAPropertyRead(["host", "hostname"])
|
||||
}
|
||||
|
||||
override string getAHeaderName() {
|
||||
@@ -597,7 +605,7 @@ module Express {
|
||||
)
|
||||
}
|
||||
|
||||
override RouteHandler getRouteHandler() { result = rh }
|
||||
override RouteHandler getRouteHandler() { result = request.getRouteHandler() }
|
||||
|
||||
override string getKind() { result = "header" }
|
||||
}
|
||||
@@ -605,14 +613,7 @@ module Express {
|
||||
/**
|
||||
* HTTP headers created by Express calls
|
||||
*/
|
||||
abstract private class ExplicitHeader extends HTTP::ExplicitHeaderDefinition {
|
||||
Expr response;
|
||||
|
||||
/**
|
||||
* Gets the response expression that this header is set on.
|
||||
*/
|
||||
Expr getResponse() { result = response }
|
||||
}
|
||||
abstract private class ExplicitHeader extends HTTP::ExplicitHeaderDefinition { }
|
||||
|
||||
/**
|
||||
* Holds if `e` is an HTTP request object.
|
||||
@@ -641,16 +642,13 @@ module Express {
|
||||
* An invocation of the `redirect` method of an HTTP response object.
|
||||
*/
|
||||
private class RedirectInvocation extends HTTP::RedirectInvocation, MethodCallExpr {
|
||||
RouteHandler rh;
|
||||
ResponseSource response;
|
||||
|
||||
RedirectInvocation() {
|
||||
getReceiver() = rh.getAResponseExpr() and
|
||||
getMethodName() = "redirect"
|
||||
}
|
||||
RedirectInvocation() { this = response.ref().getAMethodCall("redirect").asExpr() }
|
||||
|
||||
override Expr getUrlArgument() { result = getLastArgument() }
|
||||
|
||||
override RouteHandler getRouteHandler() { result = rh }
|
||||
override RouteHandler getRouteHandler() { result = response.getRouteHandler() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -668,21 +666,18 @@ module Express {
|
||||
* An invocation of the `set` or `header` method on an HTTP response object that
|
||||
* sets multiple headers.
|
||||
*/
|
||||
class SetMultipleHeaders extends ExplicitHeader, DataFlow::ValueNode {
|
||||
override MethodCallExpr astNode;
|
||||
RouteHandler rh;
|
||||
class SetMultipleHeaders extends ExplicitHeader, DataFlow::MethodCallNode {
|
||||
ResponseSource response;
|
||||
|
||||
SetMultipleHeaders() {
|
||||
astNode.getReceiver() = rh.getAResponseExpr() and
|
||||
response = astNode.getReceiver() and
|
||||
astNode.getMethodName() = any(string n | n = "set" or n = "header") and
|
||||
astNode.getNumArgument() = 1
|
||||
this = response.ref().getAMethodCall(["set", "header"]) and
|
||||
getNumArgument() = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to the multiple headers object that is to be set.
|
||||
*/
|
||||
private DataFlow::SourceNode getAHeaderSource() { result.flowsToExpr(astNode.getArgument(0)) }
|
||||
private DataFlow::SourceNode getAHeaderSource() { result.flowsTo(getArgument(0)) }
|
||||
|
||||
override predicate definesExplicitly(string headerName, Expr headerValue) {
|
||||
exists(string header |
|
||||
@@ -691,7 +686,7 @@ module Express {
|
||||
)
|
||||
}
|
||||
|
||||
override RouteHandler getRouteHandler() { result = rh }
|
||||
override RouteHandler getRouteHandler() { result = response.getRouteHandler() }
|
||||
|
||||
override Expr getNameExpr() {
|
||||
exists(DataFlow::PropWrite write | getAHeaderSource().getAPropertyWrite() = write |
|
||||
@@ -711,31 +706,26 @@ module Express {
|
||||
* An argument passed to the `send` or `end` method of an HTTP response object.
|
||||
*/
|
||||
private class ResponseSendArgument extends HTTP::ResponseSendArgument {
|
||||
RouteHandler rh;
|
||||
ResponseSource response;
|
||||
|
||||
ResponseSendArgument() {
|
||||
exists(MethodCallExpr mce |
|
||||
mce.calls(rh.getAResponseExpr(), "send") and
|
||||
this = mce.getArgument(0)
|
||||
)
|
||||
}
|
||||
ResponseSendArgument() { this = response.ref().getAMethodCall("send").getArgument(0).asExpr() }
|
||||
|
||||
override RouteHandler getRouteHandler() { result = rh }
|
||||
override RouteHandler getRouteHandler() { result = response.getRouteHandler() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An invocation of the `cookie` method on an HTTP response object.
|
||||
*/
|
||||
class SetCookie extends HTTP::CookieDefinition, MethodCallExpr {
|
||||
RouteHandler rh;
|
||||
ResponseSource response;
|
||||
|
||||
SetCookie() { calls(rh.getAResponseExpr(), "cookie") }
|
||||
SetCookie() { this = response.ref().getAMethodCall("cookie").asExpr() }
|
||||
|
||||
override Expr getNameArgument() { result = getArgument(0) }
|
||||
|
||||
override Expr getValueArgument() { result = getArgument(1) }
|
||||
|
||||
override RouteHandler getRouteHandler() { result = rh }
|
||||
override RouteHandler getRouteHandler() { result = response.getRouteHandler() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -756,11 +746,11 @@ module Express {
|
||||
* An object passed to the `render` method of an HTTP response object.
|
||||
*/
|
||||
class TemplateObjectInput extends DataFlow::Node {
|
||||
RouteHandler rh;
|
||||
ResponseSource response;
|
||||
|
||||
TemplateObjectInput() {
|
||||
exists(DataFlow::MethodCallNode render |
|
||||
render.calls(rh.getAResponseExpr().flow(), "render") and
|
||||
render = response.ref().getAMethodCall("render") and
|
||||
this = render.getArgument(1)
|
||||
)
|
||||
}
|
||||
@@ -768,7 +758,7 @@ module Express {
|
||||
/**
|
||||
* Gets the route handler that uses this object.
|
||||
*/
|
||||
RouteHandler getRouteHandler() { result = rh }
|
||||
RouteHandler getRouteHandler() { result = response.getRouteHandler() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
465
javascript/ql/src/semmle/javascript/frameworks/Nest.qll
Normal file
465
javascript/ql/src/semmle/javascript/frameworks/Nest.qll
Normal file
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* Provides classes and predicates for reasoning about [Nest](https://nestjs.com/).
|
||||
*/
|
||||
|
||||
import javascript
|
||||
private import semmle.javascript.security.dataflow.ServerSideUrlRedirectCustomizations
|
||||
|
||||
/**
|
||||
* Provides classes and predicates for reasoning about [Nest](https://nestjs.com/).
|
||||
*/
|
||||
module NestJS {
|
||||
/** Gets an API node referring to the `@nestjs/common` module. */
|
||||
private API::Node nestjs() { result = API::moduleImport("@nestjs/common") }
|
||||
|
||||
/**
|
||||
* Gets a data flow node that is applied as a decorator on the given function.
|
||||
*
|
||||
* Note that only methods in a class can have decorators.
|
||||
*/
|
||||
private DataFlow::Node getAFunctionDecorator(DataFlow::FunctionNode fun) {
|
||||
exists(MethodDefinition method |
|
||||
fun = method.getInit().flow() and
|
||||
result = method.getADecorator().getExpression().flow()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that is declared as a route handler using a decorator, for example:
|
||||
*
|
||||
* ```js
|
||||
* class C {
|
||||
* @Get('posts')
|
||||
* getPosts() { .. }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
private class NestJSRouteHandler extends HTTP::RouteHandler, DataFlow::FunctionNode {
|
||||
NestJSRouteHandler() {
|
||||
getAFunctionDecorator(this) =
|
||||
nestjs()
|
||||
.getMember(["Get", "Post", "Put", "Delete", "Patch", "Options", "Head", "All"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override HTTP::HeaderDefinition getAResponseHeader(string name) { none() }
|
||||
|
||||
/**
|
||||
* Holds if this has the `@Redirect()` decorator.
|
||||
*/
|
||||
predicate hasRedirectDecorator() {
|
||||
getAFunctionDecorator(this) = nestjs().getMember("Redirect").getACall()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the return value is sent back in the response.
|
||||
*/
|
||||
predicate isReturnValueReflected() {
|
||||
getAFunctionDecorator(this) = nestjs().getMember(["Get", "Post"]).getACall() and
|
||||
not hasRedirectDecorator() and
|
||||
not getAFunctionDecorator(this) = nestjs().getMember("Render").getACall()
|
||||
}
|
||||
|
||||
/** Gets a pipe applied to the inputs of this route handler, not including global pipes. */
|
||||
DataFlow::Node getAPipe() {
|
||||
exists(DataFlow::CallNode decorator |
|
||||
decorator = nestjs().getMember("UsePipes").getACall() and
|
||||
result = decorator.getAnArgument()
|
||||
|
|
||||
decorator = getAFunctionDecorator(this)
|
||||
or
|
||||
exists(DataFlow::ClassNode cls |
|
||||
this = cls.getAnInstanceMember() and
|
||||
decorator = cls.getADecorator()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter with a decorator that makes it receive a value derived from the incoming request.
|
||||
*
|
||||
* For example, in the following,
|
||||
* ```js
|
||||
* @Get(':foo')
|
||||
* foo(@Param('foo') foo, @Query() query) { ... }
|
||||
* ```
|
||||
* the `foo` and `query` parameters receive (part of) the path and query string, respectively.
|
||||
*/
|
||||
private class NestJSRequestInput extends DataFlow::ParameterNode {
|
||||
DataFlow::CallNode decorator;
|
||||
string decoratorName;
|
||||
|
||||
NestJSRequestInput() {
|
||||
decoratorName =
|
||||
["Query", "Param", "Headers", "Body", "HostParam", "UploadedFile", "UploadedFiles"] and
|
||||
decorator = getADecorator() and
|
||||
decorator = nestjs().getMember(decoratorName).getACall()
|
||||
}
|
||||
|
||||
/** Gets the decorator marking this as a request input. */
|
||||
DataFlow::CallNode getDecorator() { result = decorator }
|
||||
|
||||
/** Gets the route handler on which this parameter appears. */
|
||||
NestJSRouteHandler getNestRouteHandler() { result.getAParameter() = this }
|
||||
|
||||
/** Gets a pipe applied to this parameter, not including global pipes. */
|
||||
DataFlow::Node getAPipe() {
|
||||
result = getNestRouteHandler().getAPipe()
|
||||
or
|
||||
result = decorator.getArgument(1)
|
||||
or
|
||||
decorator.getNumArgument() = 1 and
|
||||
not decorator.getArgument(0).mayHaveStringValue(_) and
|
||||
result = decorator.getArgument(0) // One-argument version can either take a pipe or a property name
|
||||
}
|
||||
|
||||
/** Gets the kind of parameter, for use in `HTTP::RequestInputAccess`. */
|
||||
string getInputKind() {
|
||||
decoratorName = ["Param", "Query"] and result = "parameter"
|
||||
or
|
||||
decoratorName = ["Headers", "HostParam"] and result = "header"
|
||||
or
|
||||
decoratorName = ["Body", "UploadedFile", "UploadedFiles"] and result = "body"
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this is sanitized by a sanitizing pipe, for example, a parameter
|
||||
* with the decorator `@Param('x', ParseIntPipe)` is parsed as an integer, and
|
||||
* is thus considered to be sanitized.
|
||||
*/
|
||||
predicate isSanitizedByPipe() {
|
||||
hasSanitizingPipe(this, false)
|
||||
or
|
||||
hasSanitizingPipe(this, true) and
|
||||
isSanitizingType(getParameter().getType().unfold())
|
||||
}
|
||||
}
|
||||
|
||||
/** API node entry point for custom implementations of `ValidationPipe` (a common pattern). */
|
||||
private class ValidationNodeEntry extends API::EntryPoint {
|
||||
ValidationNodeEntry() { this = "ValidationNodeEntry" }
|
||||
|
||||
override DataFlow::SourceNode getAUse() {
|
||||
result.(DataFlow::ClassNode).getName() = "ValidationPipe"
|
||||
}
|
||||
|
||||
override DataFlow::Node getARhs() { none() }
|
||||
}
|
||||
|
||||
/** Gets an API node referring to the constructor of `ValidationPipe` */
|
||||
private API::Node validationPipe() {
|
||||
result = nestjs().getMember("ValidationPipe")
|
||||
or
|
||||
result = API::root().getASuccessor(any(ValidationNodeEntry e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pipe (instance or constructor) which causes its input to be sanitized, and thus not seen as a `RequestInputAccess`.
|
||||
*
|
||||
* If `dependsOnType` is `true`, then the validation depends on the declared type of the input,
|
||||
* and some types may not be enough to be considered sanitized.
|
||||
*/
|
||||
private API::Node sanitizingPipe(boolean dependsOnType) {
|
||||
exists(API::Node ctor |
|
||||
dependsOnType = false and
|
||||
ctor = nestjs().getMember(["ParseIntPipe", "ParseBoolPipe", "ParseUUIDPipe"])
|
||||
or
|
||||
dependsOnType = true and
|
||||
ctor = validationPipe()
|
||||
|
|
||||
result = [ctor, ctor.getInstance()]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ValidationPipe` is installed as a global pipe by a file in the given folder
|
||||
* or one of its enclosing folders.
|
||||
*
|
||||
* We use folder hierarchy to approximate the scope of globally-installed pipes.
|
||||
*/
|
||||
predicate hasGlobalValidationPipe(Folder folder) {
|
||||
exists(DataFlow::CallNode call |
|
||||
call.getCalleeName() = "useGlobalPipes" and
|
||||
call.getArgument(0) = validationPipe().getInstance().getAUse() and
|
||||
folder = call.getFile().getParentContainer()
|
||||
)
|
||||
or
|
||||
exists(API::CallNode decorator |
|
||||
decorator = nestjs().getMember("Module").getACall() and
|
||||
decorator
|
||||
.getParameter(0)
|
||||
.getMember("providers")
|
||||
.getAMember()
|
||||
.getMember("useFactory")
|
||||
.getReturn()
|
||||
.getARhs() = validationPipe().getInstance().getAUse() and
|
||||
folder = decorator.getFile().getParentContainer()
|
||||
)
|
||||
or
|
||||
hasGlobalValidationPipe(folder.getParentContainer())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `param` is affected by a pipe that sanitizes inputs.
|
||||
*/
|
||||
private predicate hasSanitizingPipe(NestJSRequestInput param, boolean dependsOnType) {
|
||||
param.getAPipe() = sanitizingPipe(dependsOnType).getAUse()
|
||||
or
|
||||
hasGlobalValidationPipe(param.getFile().getParentContainer()) and
|
||||
dependsOnType = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a parameter of type `t` is considered sanitized, provided it has been checked by `ValidationPipe`
|
||||
* (which relies on metadata emitted by the TypeScript compiler).
|
||||
*/
|
||||
private predicate isSanitizingType(Type t) {
|
||||
t instanceof NumberType
|
||||
or
|
||||
t instanceof BooleanType
|
||||
//
|
||||
// Note: we could consider types with class-validator decorators to be sanitized here, but instead we consider the root
|
||||
// object to be tainted, but omit taint steps for the individual properties names that have sanitizing decorators. See ClassValidator.qll.
|
||||
}
|
||||
|
||||
/**
|
||||
* A user-defined pipe class, for example:
|
||||
* ```js
|
||||
* class MyPipe implements PipeTransform {
|
||||
* transform(value) { return value + '!' }
|
||||
* }
|
||||
* ```
|
||||
* This can be used as a pipe, for example, `@Param('x', MyPipe)` would pipe
|
||||
* the request parameter `x` through the `transform` function before flowing into
|
||||
* the route handler.
|
||||
*/
|
||||
private class CustomPipeClass extends DataFlow::ClassNode {
|
||||
CustomPipeClass() {
|
||||
exists(ClassDefinition cls |
|
||||
this = cls.flow() and
|
||||
cls.getASuperInterface().hasQualifiedName("@nestjs/common", "PipeTransform")
|
||||
)
|
||||
}
|
||||
|
||||
DataFlow::FunctionNode getTransformFunction() { result = getInstanceMethod("transform") }
|
||||
|
||||
DataFlow::ParameterNode getInputData() { result = getTransformFunction().getParameter(0) }
|
||||
|
||||
DataFlow::Node getOutputData() { result = getTransformFunction().getReturnNode() }
|
||||
|
||||
NestJSRequestInput getAnAffectedParameter() {
|
||||
[getAnInstanceReference(), getAClassReference()].flowsTo(result.getAPipe())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The input to a custom pipe, seen as a remote flow source.
|
||||
*
|
||||
* The type of remote flow depends on which decorator is applied at the parameter, so
|
||||
* we just classify it as a `RemoteFlowSource`.
|
||||
*/
|
||||
private class NestJSCustomPipeInput extends HTTP::RequestInputAccess {
|
||||
CustomPipeClass pipe;
|
||||
|
||||
NestJSCustomPipeInput() {
|
||||
this = pipe.getInputData() and
|
||||
exists(NestJSRequestInput input |
|
||||
input = pipe.getAnAffectedParameter() and
|
||||
not input.isSanitizedByPipe()
|
||||
)
|
||||
}
|
||||
|
||||
override string getKind() {
|
||||
// Use any input kind that the pipe is applied to.
|
||||
result = pipe.getAnAffectedParameter().getInputKind()
|
||||
}
|
||||
|
||||
override HTTP::RouteHandler getRouteHandler() {
|
||||
result = pipe.getAnAffectedParameter().getNestRouteHandler()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A step from the result of a custom pipe, to an affected parameter.
|
||||
*/
|
||||
private class CustomPipeStep extends DataFlow::SharedFlowStep {
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(CustomPipeClass pipe |
|
||||
pred = pipe.getOutputData() and
|
||||
succ = pipe.getAnAffectedParameter()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A request input parameter that is not sanitized by a pipe, and therefore treated
|
||||
* as a source of untrusted data.
|
||||
*/
|
||||
private class NestJSRequestInputAsRequestInputAccess extends NestJSRequestInput,
|
||||
HTTP::RequestInputAccess {
|
||||
NestJSRequestInputAsRequestInputAccess() {
|
||||
not isSanitizedByPipe() and
|
||||
not this = any(CustomPipeClass cls).getAnAffectedParameter()
|
||||
}
|
||||
|
||||
override HTTP::RouteHandler getRouteHandler() { result = getNestRouteHandler() }
|
||||
|
||||
override string getKind() { result = getInputKind() }
|
||||
|
||||
override predicate isUserControlledObject() {
|
||||
not exists(getAPipe()) and // value is not transformed by a pipe
|
||||
(
|
||||
decorator.getNumArgument() = 0
|
||||
or
|
||||
decoratorName = ["Query", "Body"]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class NestJSHeaderAccess extends NestJSRequestInputAsRequestInputAccess,
|
||||
HTTP::RequestHeaderAccess {
|
||||
NestJSHeaderAccess() { decoratorName = "Headers" and decorator.getNumArgument() > 0 }
|
||||
|
||||
override string getAHeaderName() {
|
||||
result = decorator.getArgument(0).getStringValue().toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
private predicate isStringType(Type type) {
|
||||
type instanceof StringType
|
||||
or
|
||||
type instanceof AnyType
|
||||
or
|
||||
isStringType(type.(PromiseType).getElementType().unfold())
|
||||
}
|
||||
|
||||
/**
|
||||
* A return value from a route handler, seen as an argument to `res.send()`.
|
||||
*
|
||||
* For example,
|
||||
* ```js
|
||||
* @Get()
|
||||
* foo() {
|
||||
* return '<b>Hello</b>';
|
||||
* }
|
||||
* ```
|
||||
* writes `<b>Hello</b>` to the response.
|
||||
*/
|
||||
private class ReturnValueAsResponseSend extends HTTP::ResponseSendArgument {
|
||||
NestJSRouteHandler handler;
|
||||
|
||||
ReturnValueAsResponseSend() {
|
||||
handler.isReturnValueReflected() and
|
||||
this = handler.getAReturn().asExpr() and
|
||||
// Only returned strings are sinks
|
||||
not exists(Type type |
|
||||
type = getType() and
|
||||
not isStringType(type.unfold())
|
||||
)
|
||||
}
|
||||
|
||||
override HTTP::RouteHandler getRouteHandler() { result = handler }
|
||||
}
|
||||
|
||||
/**
|
||||
* A return value from a redirecting route handler, seen as a sink for server-side redirect.
|
||||
*
|
||||
* For example,
|
||||
* ```js
|
||||
* @Get()
|
||||
* @Redirect
|
||||
* foo() {
|
||||
* return { url: 'https://example.com' }
|
||||
* }
|
||||
* ```
|
||||
* redirects to `https://example.com`.
|
||||
*/
|
||||
private class ReturnValueAsRedirection extends ServerSideUrlRedirect::Sink {
|
||||
NestJSRouteHandler handler;
|
||||
|
||||
ReturnValueAsRedirection() {
|
||||
handler.hasRedirectDecorator() and
|
||||
this = handler.getAReturn().getALocalSource().getAPropertyWrite("url").getRhs()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter decorator created using `createParamDecorator`.
|
||||
*/
|
||||
private class CustomParameterDecorator extends API::CallNode {
|
||||
CustomParameterDecorator() { this = nestjs().getMember("createParamDecorator").getACall() }
|
||||
|
||||
/** Gets the `context` parameter. */
|
||||
API::Node getExecutionContext() { result = getParameter(0).getParameter(1) }
|
||||
|
||||
/** Gets a parameter with this decorator applied. */
|
||||
DataFlow::ParameterNode getADecoratedParameter() {
|
||||
result.getADecorator() = getReturn().getReturn().getAUse()
|
||||
}
|
||||
|
||||
/** Gets a value returned by the decorator's callback, which becomes the value of the decorated parameter. */
|
||||
DataFlow::Node getResult() { result = getParameter(0).getReturn().getARhs() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A flow step from a custom parameter decorator to a decorated parameter.
|
||||
*/
|
||||
private class CustomParameterFlowStep extends DataFlow::SharedFlowStep {
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(CustomParameterDecorator dec |
|
||||
pred = dec.getResult() and
|
||||
succ = dec.getADecoratedParameter()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private API::Node executionContext() {
|
||||
result = API::Node::ofType("@nestjs/common", "ExecutionContext")
|
||||
or
|
||||
result = any(CustomParameterDecorator d).getExecutionContext()
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of `express` request objects, based on the `@Req()` decorator,
|
||||
* or the context object in a custom decorator.
|
||||
*/
|
||||
private class ExpressRequestSource extends Express::RequestSource {
|
||||
ExpressRequestSource() {
|
||||
this.(DataFlow::ParameterNode).getADecorator() =
|
||||
nestjs().getMember(["Req", "Request"]).getReturn().getAnImmediateUse()
|
||||
or
|
||||
this =
|
||||
executionContext()
|
||||
.getMember("switchToHttp")
|
||||
.getReturn()
|
||||
.getMember("getRequest")
|
||||
.getReturn()
|
||||
.getAnImmediateUse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route handler that handles this request.
|
||||
*/
|
||||
override HTTP::RouteHandler getRouteHandler() {
|
||||
result.(DataFlow::FunctionNode).getAParameter() = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of `express` response objects, based on the `@Res()` decorator.
|
||||
*/
|
||||
private class ExpressResponseSource extends Express::ResponseSource {
|
||||
ExpressResponseSource() {
|
||||
this.(DataFlow::ParameterNode).getADecorator() =
|
||||
nestjs().getMember(["Res", "Response"]).getReturn().getAnImmediateUse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route handler that handles this request.
|
||||
*/
|
||||
override HTTP::RouteHandler getRouteHandler() {
|
||||
result.(DataFlow::FunctionNode).getAParameter() = this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,6 +465,9 @@ module NodeJSLib {
|
||||
) and
|
||||
t.start()
|
||||
or
|
||||
t.start() and
|
||||
result = DataFlow::moduleMember("fs", "promises")
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2, DataFlow::SourceNode pred | pred = fsModule(t2) |
|
||||
result = pred.track(t2, t)
|
||||
or
|
||||
|
||||
@@ -135,6 +135,8 @@ module Stages {
|
||||
exists(any(AccessPath a).getAnInstanceIn(_))
|
||||
or
|
||||
exists(any(DataFlow::PropRef ref).getBase())
|
||||
or
|
||||
exists(any(DataFlow::ClassNode cls))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,4 +67,11 @@ module LogInjection {
|
||||
class HtmlSanitizer extends Sanitizer {
|
||||
HtmlSanitizer() { this instanceof HtmlSanitizerCall }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `JSON.stringify` or similar, seen as sanitizing log output.
|
||||
*/
|
||||
class JsonStringifySanitizer extends Sanitizer {
|
||||
JsonStringifySanitizer() { this = any(JsonStringifyCall c).getOutput() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import testUtilities.ConsistencyChecking
|
||||
import semmle.javascript.security.dataflow.ReflectedXss
|
||||
import semmle.javascript.security.dataflow.ServerSideUrlRedirect
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Get, Query } from '@nestjs/common';
|
||||
import { IsIn } from 'class-validator';
|
||||
|
||||
export class Controller {
|
||||
@Get()
|
||||
route1(@Query('x') validatedObj: Struct, @Query('y') unvalidated: string) {
|
||||
if (Math.random()) return unvalidated; // NOT OK
|
||||
return validatedObj.key; // OK
|
||||
}
|
||||
}
|
||||
|
||||
class Struct {
|
||||
@IsIn(['foo', 'bar'])
|
||||
key: string;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Get, createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const SneakyQueryParam = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.query.sneakyQueryParam;
|
||||
},
|
||||
);
|
||||
|
||||
export const SafeParam = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
return 'Safe';
|
||||
},
|
||||
);
|
||||
|
||||
export class Controller {
|
||||
@Get()
|
||||
sneaky(@SneakyQueryParam() value) {
|
||||
return value; // NOT OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
safe(@SafeParam() value) {
|
||||
return value; // OK
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Get, Injectable, PipeTransform, Query, UsePipes } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CustomSanitizingPipe implements PipeTransform {
|
||||
transform(value: string): number | undefined {
|
||||
if (value == null) return undefined;
|
||||
return Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CustomPropagatingPipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
return value.toUpperCase() + '!';
|
||||
}
|
||||
}
|
||||
|
||||
export class Controller {
|
||||
@Get()
|
||||
sanitizingPipe1(@Query('x', CustomSanitizingPipe) sanitized: number): string {
|
||||
return '' + sanitized; // OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
sanitizingPipe2(@Query('x', new CustomSanitizingPipe()) sanitized: number): string {
|
||||
return '' + sanitized; // OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UsePipes(CustomSanitizingPipe)
|
||||
sanitizingPipe3(@Query('x') sanitized: number): string {
|
||||
return '' + sanitized; // OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
propagatingPipe1(@Query('x', CustomPropagatingPipe) unsanitized: string): string {
|
||||
return '' + unsanitized; // NOT OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
propagatingPipe2(@Query('x', new CustomPropagatingPipe()) unsanitized: string): string {
|
||||
return '' + unsanitized; // NOT OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UsePipes(CustomPropagatingPipe)
|
||||
propagatingPipe3(@Query('x') unsanitized: string): string {
|
||||
return '' + unsanitized; // NOT OK
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Get, Post, All, Query, Param, Body, Redirect, Req, Res, UploadedFile, UploadedFiles } from '@nestjs/common';
|
||||
import { SneakyQueryParam } from './customDecorator';
|
||||
|
||||
export class TestController {
|
||||
@Get('foo')
|
||||
getFoo() {
|
||||
return 'foo';
|
||||
}
|
||||
|
||||
@Post('foo')
|
||||
postFoo() {
|
||||
return 'foo';
|
||||
}
|
||||
|
||||
@Get()
|
||||
getRoot() {
|
||||
return 'foo';
|
||||
}
|
||||
|
||||
@All('bar')
|
||||
bar() {
|
||||
return 'bar';
|
||||
}
|
||||
|
||||
@Get('requestInputs/:x')
|
||||
requestInputs(
|
||||
@Param('x') x,
|
||||
@Query() queryObj,
|
||||
@Query('name') name,
|
||||
@Req() req
|
||||
) {
|
||||
if (Math.random()) return x; // NOT OK
|
||||
if (Math.random()) return queryObj; // NOT OK
|
||||
if (Math.random()) return name; // NOT OK
|
||||
if (Math.random()) return req.query.abc; // NOT OK
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('post')
|
||||
post(@Body() body) {
|
||||
return body.x; // NOT OK
|
||||
}
|
||||
|
||||
@Get('redir')
|
||||
@Redirect('https://example.com')
|
||||
redir() {
|
||||
return {
|
||||
url: '//other.example.com' // OK
|
||||
};
|
||||
}
|
||||
|
||||
@Get('redir')
|
||||
@Redirect('https://example.com')
|
||||
redir2(@Query('redirect') target) {
|
||||
return {
|
||||
url: target // NOT OK
|
||||
};
|
||||
}
|
||||
|
||||
@Get()
|
||||
explicitSend(@Req() req, @Res() res) {
|
||||
res.send(req.query.x) // NOT OK
|
||||
}
|
||||
|
||||
@Post()
|
||||
upload(@UploadedFile() file) {
|
||||
return file.originalname; // NOT OK
|
||||
}
|
||||
|
||||
@Post()
|
||||
uploadMany(@UploadedFiles() files) {
|
||||
return files[0].originalname; // NOT OK
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Get, Query, UsePipes, ValidationPipe } from '@nestjs/common';
|
||||
import { IsIn } from 'class-validator';
|
||||
|
||||
export class Controller {
|
||||
@Get()
|
||||
route1(@Query('x', new ValidationPipe()) validatedObj: Struct) {
|
||||
return validatedObj.key; // OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
route2(@Query('x', ValidationPipe) validatedObj: Struct) {
|
||||
return validatedObj.key; // OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UsePipes(new ValidationPipe())
|
||||
route3(@Query('x') validatedObj: Struct, @Query('y') unvalidated: string) {
|
||||
if (Math.random()) return validatedObj.key; // OK
|
||||
return unvalidated; // NOT OK
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UsePipes(ValidationPipe)
|
||||
route4(@Query('x') validatedObj: Struct, @Query('y') unvalidated: string) {
|
||||
if (Math.random()) return validatedObj.key; // OK
|
||||
return unvalidated; // NOT OK
|
||||
}
|
||||
}
|
||||
|
||||
@UsePipes(new ValidationPipe())
|
||||
export class Controller2 {
|
||||
@Get()
|
||||
route5(@Query('x') validatedObj: Struct, @Query('y') unvalidated: string) {
|
||||
if (Math.random()) return validatedObj.key; // OK
|
||||
return unvalidated; // NOT OK
|
||||
}
|
||||
}
|
||||
|
||||
@UsePipes(ValidationPipe)
|
||||
export class Controller3 {
|
||||
@Get()
|
||||
route6(@Query('x') validatedObj: Struct, @Query('y') unvalidated: string) {
|
||||
if (Math.random()) return validatedObj.key; // OK
|
||||
return unvalidated; // NOT OK
|
||||
}
|
||||
}
|
||||
|
||||
class Struct {
|
||||
@IsIn(['foo', 'bar'])
|
||||
key: string;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
routeHandler
|
||||
| global/validation.ts:6:3:9:3 | route1( ... OK\\n } |
|
||||
| local/customDecorator.ts:18:3:20:3 | sneaky( ... OK\\n } |
|
||||
| local/customDecorator.ts:23:3:25:3 | safe(@S ... OK\\n } |
|
||||
| local/customPipe.ts:20:5:22:5 | sanitiz ... K\\n } |
|
||||
| local/customPipe.ts:25:5:27:5 | sanitiz ... K\\n } |
|
||||
| local/customPipe.ts:31:5:33:5 | sanitiz ... K\\n } |
|
||||
| local/customPipe.ts:36:5:38:5 | propaga ... K\\n } |
|
||||
| local/customPipe.ts:41:5:43:5 | propaga ... K\\n } |
|
||||
| local/customPipe.ts:47:5:49:5 | propaga ... K\\n } |
|
||||
| local/routes.ts:6:3:8:3 | getFoo( ... o';\\n } |
|
||||
| local/routes.ts:11:3:13:3 | postFoo ... o';\\n } |
|
||||
| local/routes.ts:16:3:18:3 | getRoot ... o';\\n } |
|
||||
| local/routes.ts:21:3:23:3 | bar() { ... r';\\n } |
|
||||
| local/routes.ts:26:3:37:3 | request ... rn;\\n } |
|
||||
| local/routes.ts:40:3:42:3 | post(@B ... OK\\n } |
|
||||
| local/routes.ts:46:3:50:3 | redir() ... };\\n } |
|
||||
| local/routes.ts:54:3:58:3 | redir2( ... };\\n } |
|
||||
| local/routes.ts:61:3:63:3 | explici ... OK\\n } |
|
||||
| local/routes.ts:66:3:68:3 | upload( ... OK\\n } |
|
||||
| local/routes.ts:71:3:73:3 | uploadM ... OK\\n } |
|
||||
| local/validation.ts:6:3:8:3 | route1( ... OK\\n } |
|
||||
| local/validation.ts:11:3:13:3 | route2( ... OK\\n } |
|
||||
| local/validation.ts:17:3:20:3 | route3( ... OK\\n } |
|
||||
| local/validation.ts:24:3:27:3 | route4( ... OK\\n } |
|
||||
| local/validation.ts:33:3:36:3 | route5( ... OK\\n } |
|
||||
| local/validation.ts:42:3:45:3 | route6( ... OK\\n } |
|
||||
requestSource
|
||||
| local/customDecorator.ts:5:21:5:51 | ctx.swi ... quest() |
|
||||
| local/routes.ts:30:12:30:14 | req |
|
||||
| local/routes.ts:61:23:61:25 | req |
|
||||
responseSource
|
||||
| local/routes.ts:61:35:61:37 | res |
|
||||
requestInputAccess
|
||||
| body | local/routes.ts:40:16:40:19 | body |
|
||||
| body | local/routes.ts:66:26:66:29 | file |
|
||||
| body | local/routes.ts:71:31:71:35 | files |
|
||||
| parameter | global/validation.ts:6:22:6:33 | validatedObj |
|
||||
| parameter | global/validation.ts:6:56:6:66 | unvalidated |
|
||||
| parameter | local/customDecorator.ts:6:12:6:41 | request ... ryParam |
|
||||
| parameter | local/customPipe.ts:5:15:5:19 | value |
|
||||
| parameter | local/customPipe.ts:13:15:13:19 | value |
|
||||
| parameter | local/routes.ts:27:17:27:17 | x |
|
||||
| parameter | local/routes.ts:28:14:28:21 | queryObj |
|
||||
| parameter | local/routes.ts:29:20:29:23 | name |
|
||||
| parameter | local/routes.ts:35:31:35:43 | req.query.abc |
|
||||
| parameter | local/routes.ts:54:29:54:34 | target |
|
||||
| parameter | local/routes.ts:62:14:62:24 | req.query.x |
|
||||
| parameter | local/validation.ts:6:44:6:55 | validatedObj |
|
||||
| parameter | local/validation.ts:11:38:11:49 | validatedObj |
|
||||
| parameter | local/validation.ts:17:22:17:33 | validatedObj |
|
||||
| parameter | local/validation.ts:17:56:17:66 | unvalidated |
|
||||
| parameter | local/validation.ts:24:22:24:33 | validatedObj |
|
||||
| parameter | local/validation.ts:24:56:24:66 | unvalidated |
|
||||
| parameter | local/validation.ts:33:22:33:33 | validatedObj |
|
||||
| parameter | local/validation.ts:33:56:33:66 | unvalidated |
|
||||
| parameter | local/validation.ts:42:22:42:33 | validatedObj |
|
||||
| parameter | local/validation.ts:42:56:42:66 | unvalidated |
|
||||
responseSendArgument
|
||||
| global/validation.ts:7:31:7:41 | unvalidated |
|
||||
| global/validation.ts:8:12:8:27 | validatedObj.key |
|
||||
| local/customDecorator.ts:19:12:19:16 | value |
|
||||
| local/customDecorator.ts:24:12:24:16 | value |
|
||||
| local/customPipe.ts:21:16:21:29 | '' + sanitized |
|
||||
| local/customPipe.ts:26:16:26:29 | '' + sanitized |
|
||||
| local/customPipe.ts:32:16:32:29 | '' + sanitized |
|
||||
| local/customPipe.ts:37:16:37:31 | '' + unsanitized |
|
||||
| local/customPipe.ts:42:16:42:31 | '' + unsanitized |
|
||||
| local/customPipe.ts:48:16:48:31 | '' + unsanitized |
|
||||
| local/routes.ts:32:31:32:31 | x |
|
||||
| local/routes.ts:33:31:33:38 | queryObj |
|
||||
| local/routes.ts:34:31:34:34 | name |
|
||||
| local/routes.ts:35:31:35:43 | req.query.abc |
|
||||
| local/routes.ts:41:12:41:17 | body.x |
|
||||
| local/routes.ts:62:14:62:24 | req.query.x |
|
||||
| local/routes.ts:67:12:67:28 | file.originalname |
|
||||
| local/routes.ts:72:12:72:32 | files[0 ... nalname |
|
||||
| local/validation.ts:7:12:7:27 | validatedObj.key |
|
||||
| local/validation.ts:12:12:12:27 | validatedObj.key |
|
||||
| local/validation.ts:18:31:18:46 | validatedObj.key |
|
||||
| local/validation.ts:19:12:19:22 | unvalidated |
|
||||
| local/validation.ts:25:31:25:46 | validatedObj.key |
|
||||
| local/validation.ts:26:12:26:22 | unvalidated |
|
||||
| local/validation.ts:34:31:34:46 | validatedObj.key |
|
||||
| local/validation.ts:35:12:35:22 | unvalidated |
|
||||
| local/validation.ts:43:31:43:46 | validatedObj.key |
|
||||
| local/validation.ts:44:12:44:22 | unvalidated |
|
||||
redirectSink
|
||||
| local/routes.ts:48:12:48:32 | '//othe ... le.com' |
|
||||
| local/routes.ts:56:12:56:17 | target |
|
||||
19
javascript/ql/test/library-tests/frameworks/Nest/test.ql
Normal file
19
javascript/ql/test/library-tests/frameworks/Nest/test.ql
Normal file
@@ -0,0 +1,19 @@
|
||||
import javascript
|
||||
private import semmle.javascript.security.dataflow.ServerSideUrlRedirectCustomizations
|
||||
|
||||
query HTTP::RouteHandler routeHandler() { any() }
|
||||
|
||||
query HTTP::Servers::RequestSource requestSource() { any() }
|
||||
|
||||
query HTTP::Servers::ResponseSource responseSource() { any() }
|
||||
|
||||
query RemoteFlowSource requestInputAccess(string kind) {
|
||||
kind = result.(HTTP::RequestInputAccess).getKind()
|
||||
or
|
||||
not result instanceof HTTP::RequestInputAccess and
|
||||
kind = "RemoteFlowSource"
|
||||
}
|
||||
|
||||
query HTTP::ResponseSendArgument responseSendArgument() { any() }
|
||||
|
||||
query ServerSideUrlRedirect::Sink redirectSink() { any() }
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["."]
|
||||
}
|
||||
Reference in New Issue
Block a user