Merge pull request #5719 from asgerf/js/nestjs

Approved by esbena
This commit is contained in:
CodeQL CI
2021-05-11 02:08:27 -07:00
committed by GitHub
23 changed files with 997 additions and 95 deletions

View File

@@ -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

View File

@@ -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() }
}

View File

@@ -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 |

View File

@@ -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())
)
}
}

View File

@@ -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() }
}
/**

View 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
}
}
}

View File

@@ -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

View File

@@ -135,6 +135,8 @@ module Stages {
exists(any(AccessPath a).getAnInstanceIn(_))
or
exists(any(DataFlow::PropRef ref).getBase())
or
exists(any(DataFlow::ClassNode cls))
}
}

View File

@@ -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() }
}
}