Add Restify/Spife support

This commit is contained in:
Alvaro Muñoz
2022-10-03 15:50:57 +02:00
committed by Alvaro Muñoz
parent 42a97b26bb
commit 6ab62da015
18 changed files with 1775 additions and 36 deletions

View File

@@ -0,0 +1,5 @@
---
category: feature
---
- Improved support for [Restify](http://restify.com/) framework, leading to more results when scanning applications developed with this framework.

View File

@@ -0,0 +1,5 @@
---
category: feature
---
- Added support for the [Spife](https://github.com/npm/spife) framework.

View File

@@ -7,3 +7,4 @@ import semmle.javascript.frameworks.Micro
import semmle.javascript.frameworks.Restify
import semmle.javascript.frameworks.Connect
import semmle.javascript.frameworks.Fastify
import semmle.javascript.frameworks.Spife

View File

@@ -4,7 +4,11 @@
import javascript
import semmle.javascript.frameworks.HTTP
import semmle.javascript.security.dataflow.RequestForgeryCustomizations as RFC
/**
* Provides classes for working with [Restify](https://restify.com/) servers.
*/
module Restify {
/**
* An expression that creates a new Restify server.
@@ -19,23 +23,23 @@ module Restify {
/**
* A Restify route handler.
*/
class RouteHandler extends Http::Servers::StandardRouteHandler, DataFlow::ValueNode {
Function function;
RouteHandler() {
function = astNode and
any(RouteSetup setup).getARouteHandler() = this
}
abstract class RouteHandler extends Http::Servers::StandardRouteHandler, DataFlow::FunctionNode {
/**
* Gets the parameter of the route handler that contains the request object.
*/
Parameter getRequestParameter() { result = function.getParameter(0) }
DataFlow::ParameterNode getRequestParameter() { result = this.getParameter(0) }
/**
* Gets the parameter of the route handler that contains the response object.
*/
Parameter getResponseParameter() { result = function.getParameter(1) }
DataFlow::ParameterNode getResponseParameter() { result = this.getParameter(1) }
}
/**
* A standard Restify route handler.
*/
class StandardRouteHandler extends RouteHandler, DataFlow::FunctionNode {
StandardRouteHandler() { any(RouteSetup setup).getARouteHandler() = this }
}
/**
@@ -45,7 +49,7 @@ module Restify {
private class ResponseSource extends Http::Servers::ResponseSource {
RouteHandler rh;
ResponseSource() { this = DataFlow::parameterNode(rh.getResponseParameter()) }
ResponseSource() { this = rh.getResponseParameter() }
/**
* Gets the route handler that provides this response.
@@ -60,7 +64,7 @@ module Restify {
private class RequestSource extends Http::Servers::RequestSource {
RouteHandler rh;
RequestSource() { this = DataFlow::parameterNode(rh.getRequestParameter()) }
RequestSource() { this = rh.getRequestParameter() }
/**
* Gets the route handler that handles this request.
@@ -80,7 +84,7 @@ module Restify {
* A Node.js HTTP response provided by Restify.
*/
class ResponseNode extends NodeJSLib::ResponseNode {
ResponseNode() { src instanceof ResponseSource }
ResponseNode() { src instanceof ResponseSource or src instanceof FormatterResponseSource }
}
/**
@@ -95,14 +99,14 @@ module Restify {
* A Node.js HTTP request provided by Restify.
*/
class RequestNode extends NodeJSLib::RequestNode {
RequestNode() { src instanceof RequestSource }
RequestNode() { src instanceof RequestSource or src instanceof FormatterRequestSource }
}
/**
* An access to a user-controlled Restify request input.
*/
private class RequestInputAccess extends Http::RequestInputAccess {
RequestNode request;
Http::RequestNode request;
string kind;
RequestInputAccess() {
@@ -113,23 +117,17 @@ module Restify {
this.(DataFlow::PropRead).accesses(query, _)
)
or
exists(string methodName |
// `request.href()` or `request.getPath()`
kind = "url" and
this.(DataFlow::MethodCallNode).calls(request, methodName)
|
methodName = "href" or
methodName = "getPath"
exists(DataFlow::PropRead prop |
// `request.params.<name>`
// `request.query.<name>`
kind = "parameter" and
prop.accesses(request, ["params", "query"]) and
this.(DataFlow::PropRead).accesses(prop, _)
)
or
// `request.getContentType()`, `request.userAgent()`, `request.trailer(...)`, `request.header(...)`
kind = "header" and
this.(DataFlow::MethodCallNode)
.calls(request, ["getContentType", "userAgent", "trailer", "header"])
or
// `req.cookies
kind = "cookie" and
this.(DataFlow::PropRead).accesses(request, "cookies")
// `request.href()` or `request.getPath()`
kind = "url" and
this.(DataFlow::MethodCallNode).calls(request, ["href", "getPath"])
}
override RouteHandler getRouteHandler() { result = request.getRouteHandler() }
@@ -137,12 +135,35 @@ module Restify {
override string getKind() { result = kind }
}
/**
* An access to a header on a Restify request.
*/
private class RequestHeaderAccess extends Http::RequestHeaderAccess {
RouteHandler rh;
RequestHeaderAccess() {
// `request.getContentType()`, `request.userAgent()`, `request.trailer(...)`, `request.header(...)`
this =
rh.getARequestSource()
.ref()
.getAMethodCall(["header", "trailer", "userAgent", "getContentType"])
}
override string getAHeaderName() {
result = this.(DataFlow::MethodCallNode).getArgument(0).getStringValue().toLowerCase()
}
override RouteHandler getRouteHandler() { result = rh }
override string getKind() { result = "header" }
}
/**
* An HTTP header defined in a Restify server.
*/
private class HeaderDefinition extends Http::Servers::StandardHeaderDefinition {
HeaderDefinition() {
// response.header('Cache-Control', 'no-cache')
// res.header('Cache-Control', 'no-cache')
this.getReceiver() instanceof ResponseNode and
this.getMethodName() = "header"
}
@@ -150,6 +171,41 @@ module Restify {
override RouteHandler getRouteHandler() { this.getReceiver() = result.getAResponseNode() }
}
/**
* An invocation that sets any number of headers of the HTTP response.
*/
private class MultipleHeaderDefinitions extends Http::ExplicitHeaderDefinition,
DataFlow::MethodCallNode {
MultipleHeaderDefinitions() {
// res.set({'Cache-Control': 'no-cache'})
this.getReceiver() instanceof ResponseNode and
this.getMethodName() = "set"
}
/**
* Gets a reference to the multiple headers object that is to be set.
*/
private DataFlow::SourceNode getAHeaderSource() {
this.getArgument(0).getALocalSource() instanceof DataFlow::ObjectLiteralNode and
result.flowsTo(this.getArgument(0))
}
override predicate definesHeaderValue(string headerName, DataFlow::Node headerValue) {
exists(string header |
this.getAHeaderSource().hasPropertyWrite(header, headerValue) and
headerName = header.toLowerCase()
)
}
override DataFlow::Node getNameNode() {
exists(DataFlow::PropWrite write | this.getAHeaderSource().getAPropertyWrite() = write |
result = write.getPropertyNameExpr().flow()
)
}
override RouteHandler getRouteHandler() { this.getReceiver() = result.getAResponseNode() }
}
/**
* A call to a Restify method that sets up a route.
*/
@@ -157,13 +213,263 @@ module Restify {
ServerDefinition server;
RouteSetup() {
// server.get('/', fun)
// server.head('/', fun)
server.ref().getAMethodCall(any(Http::RequestMethodName m).toLowerCase()) = this
server
.ref()
.getAMethodCall([
"del", "get", "head", "opts", "post", "put", "patch", "param", "pre", "use", "on"
]) = this
}
override DataFlow::SourceNode getARouteHandler() { result.flowsTo(this.getArgument(1)) }
override DataFlow::SourceNode getARouteHandler() {
exists(DataFlow::Node arg |
// server.get('/', fun)
// server.get('/', fun1, fun2)
// server.get('/', [fun1, fun2])
// server.param('name', fun)
// server.on('event', fun)
this.getMethodName() = ["del", "get", "head", "opts", "post", "put", "patch", "param", "on"] and
arg = this.getAnArgument() and
not arg = this.getArgument(0)
or
// server.use(fun)
// server.use(fun1, fun2)
// server.use([fun1, fun2])
this.getMethodName() = ["use", "pre"] and
arg = this.getAnArgument()
|
(
// server.use(fun1, fun2)
result.flowsTo(arg) and
not arg.getALocalSource() instanceof DataFlow::ArrayCreationNode
or
result.flowsTo(arg.getALocalSource().(DataFlow::ArrayCreationNode).getAnElement())
)
)
}
override DataFlow::Node getServer() { result = server }
}
/**
* A call to a Restify's createServer method that sets up a formatter.
*/
class FormatterSetup extends DataFlow::CallNode {
DataFlow::ObjectLiteralNode formatters;
FormatterSetup() {
// `server = restify.createServer({ formatters: { ... } })`
this = DataFlow::moduleMember("restify", "createServer").getACall() and
this.getArgument(0)
.getALocalSource()
.(DataFlow::ObjectLiteralNode)
.hasPropertyWrite("formatters", formatters)
}
DataFlow::SourceNode getAFormatterHandler() { formatters.hasPropertyWrite(_, result) }
}
/**
* A Restify route handler.
*/
class FormatterHandler extends Http::Servers::StandardRouteHandler, DataFlow::FunctionNode {
Function function;
FormatterHandler() {
function = astNode and
any(FormatterSetup setup).getAFormatterHandler() = this
}
/**
* Gets the parameter of the formatter handler that contains the request object.
*/
Parameter getRequestParameter() { result = function.getParameter(0) }
/**
* Gets the parameter of the formatter handler that contains the response object.
*/
Parameter getResponseParameter() { result = function.getParameter(1) }
/**
* Gets the parameter of the formatter handler that contains the body object.
*/
Parameter getBodyParameter() { result = function.getParameter(2) }
}
/**
* A Restify request source, that is, the request parameter of a
* route handler.
*/
private class FormatterRequestSource extends Http::Servers::RequestSource {
FormatterHandler fh;
FormatterRequestSource() { this = DataFlow::parameterNode(fh.getRequestParameter()) }
/**
* Gets the formatter handler that handles this request.
*/
override RouteHandler getRouteHandler() { result = fh }
}
/**
* A Restify response source, that is, the response parameter of a
* route handler.
*/
private class FormatterResponseSource extends Http::Servers::ResponseSource {
FormatterHandler fh;
FormatterResponseSource() { this = DataFlow::parameterNode(fh.getResponseParameter()) }
/**
* Gets the route handler that provides this response.
*/
override RouteHandler getRouteHandler() { result = fh }
}
/**
* An argument passed to the `send` method of an HTTP response object.
*/
private class ResponseSendArgument extends Http::ResponseSendArgument {
RouteHandler rh;
ResponseSendArgument() {
this = rh.getAResponseSource().ref().getAMethodCall(["send", "sendRaw"]).getArgument(0)
}
override RouteHandler getRouteHandler() { result = rh }
}
/**
* An expression returned by a formatter
*/
private class FormatterOutput extends Http::ResponseSendArgument {
FormatterHandler fh;
FormatterOutput() { this = fh.getAReturn() }
override Http::RouteHandler getRouteHandler() { result = fh }
}
/**
* An invocation of the `redirect` method of an HTTP response object.
*/
private class RedirectInvocation extends Http::RedirectInvocation, DataFlow::MethodCallNode {
RouteHandler rh;
RedirectInvocation() { this = rh.getAResponseSource().ref().getAMethodCall("redirect") }
override DataFlow::Node getUrlArgument() {
this.getNumArgument() = 3 and
result = this.getArgument(1)
or
this.getNumArgument() = 2 and
this.getArgument(0)
.getALocalSource()
.(DataFlow::ObjectLiteralNode)
.hasPropertyWrite("hostname", result)
or
this.getNumArgument() = 2 and
result = this.getArgument(0)
}
override RouteHandler getRouteHandler() { result = rh }
}
/**
* A function that looks like a Restify route handler.
*
* For example, this could be the function `function(req, res, next){...}`.
*/
class RouteHandlerCandidate extends Http::RouteHandlerCandidate {
RouteHandlerCandidate() {
// heuristic: parameter names match the Restify documentation
astNode.getNumParameter() = [2, 3] and
astNode.getParameter(0).getName() = ["request", "req"] and
astNode.getParameter(1).getName() = ["response", "res"] and
not astNode.getParameter(2).getName() != "next" and
// heuristic: is not invoked (Restify invokes this at a call site we cannot reason precisely about)
not exists(DataFlow::InvokeNode cs | cs.getACallee() = astNode)
}
}
/**
* The URL of a REstify client, viewed as a sink for request forgery.
*/
class RequestForgerySink extends RFC::RequestForgery::Sink {
RequestForgerySink() {
exists(DataFlow::Node arg |
DataFlow::moduleMember("restify-clients",
["createClient", "createJsonClient", "createStringClient"]).getACall().getArgument(0) =
arg and
(
arg.getALocalSource().(DataFlow::ObjectLiteralNode).hasPropertyWrite("url", this)
or
not arg.getALocalSource() instanceof DataFlow::ObjectLiteralNode and
this = arg
)
)
}
override DataFlow::Node getARequest() {
// returning the createClient argument itself since there is no request associated to the client yet.
// `getARequest()` is only used for display purposes
result = this
}
override string getKind() { result = "host" }
}
/**
* A header produced by a formatter
*/
private class FormatterContentTypeHeader extends Http::ImplicitHeaderDefinition,
DataFlow::FunctionNode instanceof FormatterHandler {
string contentType;
FormatterContentTypeHeader() {
exists(DataFlow::PropWrite write |
write.getRhs() = this and
write.getPropertyName() = contentType
)
}
override predicate defines(string headerName, string headerValue) {
headerName = "content-type" and headerValue = contentType
}
override Http::RouteHandler getRouteHandler() { result = this }
}
/**
* A header produced by a route handler with no explicit declaration of a Content-Type.
*/
private class ContentTypeRouteHandlerHeader extends Http::ImplicitHeaderDefinition,
DataFlow::FunctionNode {
ContentTypeRouteHandlerHeader() { this instanceof RouteHandler }
override predicate defines(string headerName, string headerValue) {
headerName = "content-type" and headerValue = "application/json"
}
override Http::RouteHandler getRouteHandler() { result = this }
}
/** A Restify router */
private class RouterRange extends Routing::Router::Range {
ServerDefinition def;
RouterRange() { this = def }
override DataFlow::SourceNode getAReference() { result = def.ref() }
}
private class RoutingTreeSetup extends Routing::RouteSetup::MethodCall {
RoutingTreeSetup() { this instanceof RouteSetup }
override string getRelativePath() {
not this.getMethodName() = ["use", "pre", "param", "on"] and // do not treat parameter name as a path
result = this.getArgument(0).getStringValue()
}
override Http::RequestMethodName getHttpMethod() { result.toLowerCase() = this.getMethodName() }
}
}

View File

@@ -0,0 +1,440 @@
/**
* Provides classes for working with [Spife](https://github.com/npm/spife) applications.
*/
import javascript
import semmle.javascript.frameworks.HTTP
/**
* Provides classes for working with [Spife](https://github.com/npm/spife) applications.
*/
module Spife {
/**
* An API graph entry point ensuring all tagged template exprs are part of the API graph
*/
private class TaggedTemplateEntryPoint extends API::EntryPoint {
TaggedTemplateEntryPoint() { this = "TaggedTemplateEntryPoint" }
override DataFlow::SourceNode getASource() { result.asExpr() instanceof TaggedTemplateExpr }
}
/**
* A call to a Spife method that sets up a route.
*/
private class RouteSetup extends API::CallNode, Http::Servers::StandardRouteSetup {
TaggedTemplateExpr template;
RouteSetup() {
exists(CallExpr templateCall |
this.getCalleeNode().asExpr() = template and
API::moduleImport(["@npm/spife/routing", "spife/routing"])
.asSource()
.flowsToExpr(template.getTag()) and
templateCall.getAChild() = template
)
}
private string getRoutePattern() {
// Concatenate the constant parts of the expression
result =
concat(Expr e, int i |
e = template.getTemplate().getElement(i) and exists(e.getStringValue())
|
e.getStringValue() order by i
)
}
private string getARouteLine() {
result = this.getRoutePattern().splitAt("\n").regexpReplaceAll(" +", " ").trim()
}
private predicate hasLine(string method, string path, string handlerName) {
exists(string line | line = this.getARouteLine() |
line.splitAt(" ", 0) = method and
line.splitAt(" ", 1) = path and
line.splitAt(" ", 2) = handlerName
)
}
API::Node getHandlerByName(string name) { result = this.getParameter(0).getMember(name) }
API::Node getHandlerByRoute(string method, string path) {
exists(string handlerName |
this.hasLine(method, path, handlerName) and
result = this.getHandlerByName(handlerName)
)
}
override DataFlow::SourceNode getARouteHandler() {
result = this.getHandlerByRoute(_, _).getAValueReachingSink().(DataFlow::FunctionNode)
or
exists(DataFlow::MethodCallNode validation |
validation = this.getHandlerByRoute(_, _).getAValueReachingSink() and
result = validation.getArgument(1).getAFunctionValue()
)
}
override DataFlow::Node getServer() { none() }
}
/**
* A Spife route handler.
*/
abstract class RouteHandler extends Http::Servers::StandardRouteHandler, DataFlow::FunctionNode {
/**
* Gets the parameter of the route handler that contains the request object.
*/
DataFlow::ParameterNode getRequestParameter() { result = this.getParameter(0) }
/**
* Gets the parameter of the route handler that contains the context object.
*/
DataFlow::ParameterNode getContextParameter() { result = this.getParameter(1) }
}
/**
* A standard Spife route handler.
*/
private class StandardRouteHandler extends RouteHandler, DataFlow::FunctionNode {
StandardRouteHandler() { any(RouteSetup setup).getARouteHandler() = this }
}
/**
* A function that looks like a Spife route handler.
*
* For example, this could be the function `function(req, res, next){...}`.
*/
class RouteHandlerCandidate extends Http::RouteHandlerCandidate {
RouteHandlerCandidate() {
// heuristic: parameter names match the Restify documentation
astNode.getNumParameter() = 2 and
astNode.getParameter(0).getName() = ["request", "req"] and
astNode.getParameter(1).getName() = ["context", "ctx"]
}
}
/**
* A Spife request source, that is, the request parameter of a
* route handler.
*/
private class RequestSource extends Http::Servers::RequestSource {
RouteHandler rh;
RequestSource() { this = rh.getRequestParameter() }
/**
* Gets the route handler that handles this request.
*/
override RouteHandler getRouteHandler() { result = rh }
}
/**
* A Spife context source, that is, the context the parameter of a
* route handler.
*/
private class ContextSource extends Http::Servers::RequestSource {
RouteHandler rh;
ContextSource() { this = rh.getContextParameter() }
/**
* Gets the route handler that handles this request.
*/
override RouteHandler getRouteHandler() { result = rh }
}
/**
* An access to a user-controlled Spife request input.
*/
private class RequestInputAccess extends Http::RequestInputAccess {
RouteHandler rh;
string kind;
RequestInputAccess() {
this = rh.getARequestSource().ref().getAPropertyRead("body") and
kind = "body"
or
this = rh.getARequestSource().ref().getAPropertyRead("query").getAPropertyRead() and
kind = "parameter"
or
this = rh.getARequestSource().ref().getAPropertyRead("raw") and
kind = "raw"
or
this = rh.getARequestSource().ref().getAPropertyRead(["url", "urlObject"]) and
kind = "url"
or
this = rh.getARequestSource().ref().getAMethodCall() and
this.(DataFlow::MethodCallNode).getMethodName() = ["cookie", "cookies"] and
kind = "cookie"
or
exists(DataFlow::PropRead validated, DataFlow::MethodCallNode get |
rh.getARequestSource().ref().getAPropertyRead() = validated and
validated.getPropertyName().matches("validated%") and
get.getReceiver() = validated and
this = get and
kind = "body"
)
}
override RouteHandler getRouteHandler() { result = rh }
override string getKind() { result = kind }
}
/**
* An access to a user-controlled Spife context input.
*/
private class ContextInputAccess extends Http::RequestInputAccess {
ContextSource request;
string kind;
ContextInputAccess() {
request.ref().flowsTo(this.(DataFlow::MethodCallNode).getReceiver()) and
this.(DataFlow::MethodCallNode).getMethodName() = "get" and
kind = "path"
}
override RouteHandler getRouteHandler() { result = request.getRouteHandler() }
override string getKind() { result = kind }
}
/**
* An access to a header on a Spife request.
*/
private class RequestHeaderAccess extends Http::RequestHeaderAccess {
RouteHandler rh;
RequestHeaderAccess() {
this =
rh.getARequestSource().ref().getAPropertyRead(["headers", "rawHeaders"]).getAPropertyRead()
}
override string getAHeaderName() {
result = this.(DataFlow::PropRead).getPropertyName().toLowerCase()
}
override RouteHandler getRouteHandler() { result = rh }
override string getKind() { result = "header" }
}
/**
* A Spife response source, that is, the response variable used by a
* route handler.
*/
private class ReplySource extends Http::Servers::ResponseSource {
ReplySource() {
// const reply = require("@npm/spife/reply")
// reply(resp)
// reply.header(resp, 'foo', 'bar')
this = API::moduleImport(["@npm/spife/reply", "spife/reply"]).getACall() or
this = API::moduleImport(["@npm/spife/reply", "spife/reply"]).getAMember().getACall()
}
private DataFlow::SourceNode reachesHandlerReturn(
DataFlow::CallNode headerCall, DataFlow::TypeTracker t
) {
result = headerCall and
t.start()
or
exists(DataFlow::TypeTracker t2 |
result = this.reachesHandlerReturn(headerCall, t2).track(t2, t)
)
}
/**
* Gets the route handler that provides this response.
*/
override RouteHandler getRouteHandler() {
exists(RouteHandler handler |
handler.(DataFlow::FunctionNode).getAReturn().getALocalSource() =
this.reachesHandlerReturn(this, DataFlow::TypeTracker::end()) and
result = handler
)
}
}
/**
* An HTTP header defined in a Spife response.
*/
private class HeaderDefinition extends Http::ExplicitHeaderDefinition, DataFlow::MethodCallNode {
ReplySource reply;
HeaderDefinition() {
// reply.header(RESPONSE, 'Cache-Control', 'no-cache')
reply.ref().(DataFlow::MethodCallNode).getMethodName() = "header" and
reply.ref().(DataFlow::MethodCallNode).getNumArgument() = 3 and
this = reply
}
override predicate definesHeaderValue(string headerName, DataFlow::Node headerValue) {
// reply.header(RESPONSE, 'Cache-Control', 'no-cache')
headerName = this.getNameNode().getStringValue() and
headerValue = this.getArgument(2)
}
override DataFlow::Node getNameNode() { result = this.getArgument(1) }
override RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
}
/**
* An invocation that sets any number of headers of the HTTP response.
*/
private class MultipleHeaderDefinitions extends Http::ExplicitHeaderDefinition, DataFlow::CallNode {
ReplySource reply;
MultipleHeaderDefinitions() {
// reply.header(RESPONSE, {'Cache-Control': 'no-cache'})
// reply(RESPONSE, {'Cache-Control': 'no-cache'})
reply.ref().(DataFlow::CallNode).getCalleeName() = ["header", "reply"] and
reply.ref().(DataFlow::CallNode).getAnArgument().getALocalSource() instanceof
DataFlow::ObjectLiteralNode and
this = reply
}
/**
* Gets a reference to the multiple headers object that is to be set.
*/
private DataFlow::SourceNode getAHeaderSource() {
exists(int i |
this.getArgument(i).getALocalSource() instanceof DataFlow::ObjectLiteralNode and
result.flowsTo(this.getArgument(i))
)
}
override predicate definesHeaderValue(string headerName, DataFlow::Node headerValue) {
exists(string header |
this.getAHeaderSource().hasPropertyWrite(header, headerValue) and
headerName = header.toLowerCase()
)
}
override DataFlow::Node getNameNode() {
exists(DataFlow::PropWrite write | this.getAHeaderSource().getAPropertyWrite() = write |
result = write.getPropertyNameExpr().flow()
)
}
override RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
}
/**
* A header produced by a route handler with no explicit declaration of a Content-Type.
*/
private class ContentTypeRouteHandlerHeader extends Http::ImplicitHeaderDefinition,
DataFlow::FunctionNode {
ContentTypeRouteHandlerHeader() { this instanceof RouteHandler }
override predicate defines(string headerName, string headerValue) {
headerName = "content-type" and headerValue = "application/json"
}
override Http::RouteHandler getRouteHandler() { result = this }
}
/**
* An HTTP cookie defined in a Spife HTTP response.
*/
private class CookieDefinition extends Http::CookieDefinition, DataFlow::MethodCallNode {
ReplySource reply;
CookieDefinition() {
// reply.cookie(RESPONSE, 'TEST', 'FOO', {"maxAge": 1000, "httpOnly": true, "secure": true})
this = reply.ref().(DataFlow::MethodCallNode) and
this.getMethodName() = "cookie"
}
override DataFlow::Node getNameArgument() { result = this.getArgument(1) }
override DataFlow::Node getValueArgument() { result = this.getArgument(2) }
override RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
}
/**
* A response argument passed to the `reply` method.
*/
private class ReplyArgument extends Http::ResponseSendArgument, DataFlow::Node {
RouteHandler rh;
ReplyArgument() {
exists(ReplySource reply |
reply.ref().(DataFlow::CallNode).getCalleeName() =
["reply", "cookie", "link", "header", "headers", "raw", "status", "toStream", "vary"] and
this = reply.ref().(DataFlow::CallNode).getArgument(0) and
rh = reply.getRouteHandler()
)
or
this = rh.(DataFlow::FunctionNode).getAReturn()
}
override RouteHandler getRouteHandler() { result = rh }
}
/**
* An expression passed to the `template` method of the reply object
* as the value of a template variable.
*/
private class TemplateInput extends Http::ResponseBody {
TemplateObjectInput obj;
TemplateInput() {
obj.getALocalSource().(DataFlow::ObjectLiteralNode).hasPropertyWrite(_, this)
}
override RouteHandler getRouteHandler() { result = obj.getRouteHandler() }
}
/**
* An object passed to the `template` method of the reply object.
*/
private class TemplateObjectInput extends DataFlow::Node {
ReplySource reply;
TemplateObjectInput() {
reply.ref().(DataFlow::MethodCallNode).getMethodName() = "template" and
this = reply.ref().(DataFlow::MethodCallNode).getArgument(1)
}
/**
* Gets the route handler that uses this object.
*/
RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
}
/**
* An invocation of the `redirect` method of an HTTP response object.
*/
private class RedirectInvocation extends Http::RedirectInvocation, DataFlow::MethodCallNode {
ReplySource reply;
RedirectInvocation() {
this = reply.ref().(DataFlow::MethodCallNode) and
this.getMethodName() = "redirect"
}
override DataFlow::Node getUrlArgument() { result = this.getAnArgument() }
override RouteHandler getRouteHandler() { result = reply.getRouteHandler() }
}
/**
* A call to `reply.template('template', { ... })`, seen as a template instantiation.
*/
private class TemplateCall extends Templating::TemplateInstantiation::Range, DataFlow::CallNode {
TemplateCall() {
exists(ReplySource reply |
reply.ref().(DataFlow::MethodCallNode).getMethodName() = "template" and
this = reply.ref()
)
}
override DataFlow::SourceNode getOutput() { result = this }
override DataFlow::Node getTemplateFileNode() { result = this.getArgument(0) }
override DataFlow::Node getTemplateParamsNode() { result = this.getArgument(1) }
}
}

View File

@@ -45,3 +45,19 @@ private class PromotedConnectCandidate extends Connect::RouteHandler,
result = ConnectExpressShared::getRouteHandlerParameter(this, kind)
}
}
/**
* Add `Restify::RouteHandlerCandidate` to the extent of `Restify::RouteHandler`.
*/
private class PromotedRestifyCandidate extends Restify::RouteHandler,
Http::Servers::StandardRouteHandler {
PromotedRestifyCandidate() { this instanceof Restify::RouteHandlerCandidate }
}
/**
* Add `Spife::RouteHandlerCandidate` to the extent of `Spife::RouteHandler`.
*/
private class PromotedSpifeCandidate extends Spife::RouteHandler,
Http::Servers::StandardRouteHandler {
PromotedSpifeCandidate() { this instanceof Spife::RouteHandlerCandidate }
}

View File

@@ -35,4 +35,3 @@
| restify.js:9:5:9:17 | req.trailer() | header |
| restify.js:10:5:10:16 | req.header() | header |
| restify.js:11:5:11:11 | req.url | url |
| restify.js:12:5:12:15 | req.cookies | cookie |

View File

@@ -299,11 +299,13 @@ test_RemoteFlowSources
| src/http.js:30:28:30:32 | chunk |
| src/http.js:40:23:40:30 | authInfo |
| src/http.js:45:23:45:27 | error |
| src/http.js:63:17:63:33 | req.query.myParam |
| src/http.js:73:18:73:22 | chunk |
| src/http.js:82:18:82:22 | chunk |
| src/https.js:6:26:6:32 | req.url |
| src/https.js:8:3:8:20 | req.headers.cookie |
| src/https.js:9:3:9:17 | req.headers.foo |
| src/indirect2.js:10:12:10:25 | req.params.key |
| src/indirect.js:17:28:17:34 | req.url |
test_RouteHandler
| createServer.js:2:20:2:41 | functio ... res) {} | createServer.js:2:1:2:42 | https.c ... es) {}) |

View File

@@ -0,0 +1,207 @@
var restify = require('restify');
const restifyPlugins = require('restify-plugins');
var clients = require('restify-clients');
const opts = {
formatters: {
'text/plain': function(req, res, body) { // test: formatter
if (body instanceof Error) {
return '<html><body>' + body.message + '</body></html>'; // test: stackTraceExposureSink
} else {
return '<html><body>' + body + req.params.name + '</body></html>'; // test: source, stackTraceExposureSink, !xssSink, !xss
}
}
}
}
const _server = restify.createServer(opts)
const server = restify.createServer({
formatters: {
'text/html': function(req, res, body) { // test: formatter
if (body instanceof Error) {
return '<html><body>' + body.message + '</body></html>'; // test: stackTraceExposureSink, xssSink
} else {
return '<html><body>' + body + req.params.name + '</body></html>'; // test: source, stackTraceExposureSink, xssSink, xss
}
}
}
});
// The pre handler chain is executed before routing. That means these handlers will execute for an incoming request even if its for a route that you did not register.
server.pre(restify.plugins.pre.dedupeSlashes());
server.pre(function(req, res, next) { // test: handler
return next();
});
// The use handler chains is executed after a route has been chosen to service the request.
server.use(restifyPlugins.jsonBodyParser({ mapParams: true })); // TODO: prototype pollution?
server.use(restifyPlugins.acceptParser(server.acceptable));
server.use(restifyPlugins.queryParser({ mapParams: true })); // TODO: prototype pollution?
server.use(restifyPlugins.fullResponse());
server.use(function(req, res, next) { // test: handler
return next();
});
function filter(req, res, next) { // test: handler
return next();
}
function filter1(req, res, next) { // test: handler
return next();
}
function filter2(req, res, next) { // test: handler
return next();
}
function filter3(req, res, next) { // test: handler
return next();
}
function filter4(req, res, next) { // test: handler
return next();
}
function filter5(req, res, next) { // test: handler
return next();
}
function filter6(req, res, next) { // test: handler
return next();
}
const handlers = [filter5, filter6];
server.use(filter); // test: setup
server.use(filter1, filter2); // test: setup
server.use([filter3, filter4]); // test: setup
server.use(handlers); // setup
function respond(req, res, next) { // test: handler
res.send('hello ' + req.params.name); // test: source, stackTraceExposureSink
res.send('hello ' + req.params["name"]); // test: source, stackTraceExposureSink
res.send('hello ' + req.query.name); // test: source, stackTraceExposureSink
res.send('hello ' + req.params[0]); // test: source, stackTraceExposureSink
res.redirect({
hostname: req.params.name, // test: source, redirectSink
pathname: '/bar',
port: 80,
secure: true,
permanent: true,
query: {
a: 1
}
}, next);
res.redirect(301, req.params.name, next); // test: source, redirectSink
res.redirect(req.params.name, next); // test: source, redirectSink
next();
}
server.get('/hello/:name', respond); // test: setup
server.head('/hello/:name', respond); // test: setup
server.get('/', function(req, res, next) { // test: setup, handler
res.send('home')
return next();
});
server.get('/foo', // test: setup
function(req, res, next) { // test: handler
req.someData = req.params.name; // test: source
return next();
},
function(req, res, next) { // test: handler
res.header("Content-Type", "text/html");
res.send(req.someData); // test: stackTraceExposureSink, xssSink, xss
return next();
}
);
server.get('/foo2', // test: setup
[function(req, res, next) { // test: handler
req.someData = 'foo';
return next();
},
function(req, res, next) { // test: handler
res.send(req.someData); // test: stackTraceExposureSink
return next();
}]
);
function xss(req, res, next) { // test: handler
res.header("Content-Type", "text/html");
res.send('hello ' + req.query.name); // test: source, stackTraceExposureSink, xssSink, xss
next();
}
server["get"]('/xss', xss); // test: setup
function xss2(req, res, next) { // test: handler
var body = req.params.name; // test: source
res.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/html'
});
res.write(body); // test: stackTraceExposureSink, xssSink, xss
res.end();
next();
}
["get", "head"].forEach(method => {
server[method]('/xss2', xss2);
});
function xss3(req, res, next) { // test: handler
res.header("Content-Type", "text/html");
res.send('hello ' + req.header("foo")); // test: source, stackTraceExposureSink, xssSink, !xss
next();
}
server["get"]('/xss3', xss3); // test: setup
function sendV2(req, res, next) { // test: candidateHandler
res.set({
"Content-Type": "text/html",
"access-control-allow-origin": "*", // test: corsMiconfigurationSink
"access-control-allow-headers": "Content-Type, Authorization, Content-Length, X-Requested-With",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-credentials": "true"
})
res.send('hello ' + req.params.name); // test: source, stackTraceExposureSink, xssSink, xss
clients.createJsonClient({
url: req.params.uri, // test: source, ssrfSink
});
clients.createJsonClient(req.params.uri); // test: source, ssrfSink
next();
}
server.get('/hello2/:name', restify.plugins.conditionalHandler([ // test: setup
{ version: ['2.0.0', '2.1.0', '2.2.0'], handler: sendV2 }
]));
server.get('/version/test', restify.plugins.conditionalHandler([ //test: setup
{
version: ['2.0.0', '2.1.0', '2.2.0'],
handler: function(req, res, next) { // test: candidateHandler
res.send(200, {
requestedVersion: req.version(),
matchedVersion: req.matchedVersion()
});
return next();
}
}
]));
server.on('InternalServer', function(req, res, err, callback) { // test: setup, handler
return callback();
});
server.on('restifyError', function(req, res, err, callback) { // test: setup, handler
return callback();
});
server.on('after', function(req, res, route, error) { // test: setup, handler
});
server.on('pre', function(req, res) { // test: setup, handler
});
server.on('routed', function(req, res, route) { // test: setup, handler
res.header("Content-Type", "text/plain")
res.send(req.params.foo) // test: source, !xssSink, !xss
});
server.on('uncaughtException', function(req, res, route, err) { // test: setup, handler
res.header("Content-Type", "text/html")
res.send(req.params.foo) // test: source, xssSink, xss
});
server.listen(8080, function() {
console.log('%s listening at %s', server.name, server.url);
});

View File

@@ -0,0 +1,105 @@
passingPositiveTests
| PASSED | candidateHandler | src/index.js:150:35:150:59 | // test ... Handler |
| PASSED | candidateHandler | src/index.js:173:42:173:66 | // test ... Handler |
| PASSED | corsMiconfigurationSink | src/index.js:153:41:153:72 | // test ... ionSink |
| PASSED | handler | src/index.js:32:39:32:54 | // test: handler |
| PASSED | handler | src/index.js:41:39:41:54 | // test: handler |
| PASSED | handler | src/index.js:44:35:44:50 | // test: handler |
| PASSED | handler | src/index.js:47:36:47:51 | // test: handler |
| PASSED | handler | src/index.js:50:36:50:51 | // test: handler |
| PASSED | handler | src/index.js:53:36:53:51 | // test: handler |
| PASSED | handler | src/index.js:56:36:56:51 | // test: handler |
| PASSED | handler | src/index.js:59:36:59:51 | // test: handler |
| PASSED | handler | src/index.js:62:36:62:51 | // test: handler |
| PASSED | handler | src/index.js:71:36:71:51 | // test: handler |
| PASSED | handler | src/index.js:93:44:93:66 | // test ... handler |
| PASSED | handler | src/index.js:99:30:99:45 | // test: handler |
| PASSED | handler | src/index.js:103:30:103:45 | // test: handler |
| PASSED | handler | src/index.js:111:31:111:46 | // test: handler |
| PASSED | handler | src/index.js:115:30:115:45 | // test: handler |
| PASSED | handler | src/index.js:121:32:121:47 | // test: handler |
| PASSED | handler | src/index.js:128:33:128:48 | // test: handler |
| PASSED | handler | src/index.js:142:33:142:48 | // test: handler |
| PASSED | handler | src/index.js:183:65:183:87 | // test ... handler |
| PASSED | handler | src/index.js:187:63:187:85 | // test ... handler |
| PASSED | handler | src/index.js:190:55:190:77 | // test ... handler |
| PASSED | handler | src/index.js:192:39:192:61 | // test ... handler |
| PASSED | handler | src/index.js:194:49:194:71 | // test ... handler |
| PASSED | handler | src/index.js:198:65:198:87 | // test ... handler |
| PASSED | redirectSink | src/index.js:78:32:78:60 | // test ... ectSink |
| PASSED | redirectSink | src/index.js:87:45:87:73 | // test ... ectSink |
| PASSED | redirectSink | src/index.js:88:40:88:68 | // test ... ectSink |
| PASSED | setup | src/index.js:66:21:66:34 | // test: setup |
| PASSED | setup | src/index.js:67:31:67:44 | // test: setup |
| PASSED | setup | src/index.js:68:33:68:46 | // test: setup |
| PASSED | setup | src/index.js:91:38:91:51 | // test: setup |
| PASSED | setup | src/index.js:92:39:92:52 | // test: setup |
| PASSED | setup | src/index.js:93:44:93:66 | // test ... handler |
| PASSED | setup | src/index.js:98:20:98:33 | // test: setup |
| PASSED | setup | src/index.js:110:21:110:34 | // test: setup |
| PASSED | setup | src/index.js:126:29:126:42 | // test: setup |
| PASSED | setup | src/index.js:147:31:147:44 | // test: setup |
| PASSED | setup | src/index.js:166:66:166:79 | // test: setup |
| PASSED | setup | src/index.js:170:66:170:78 | //test: setup |
| PASSED | setup | src/index.js:183:65:183:87 | // test ... handler |
| PASSED | setup | src/index.js:187:63:187:85 | // test ... handler |
| PASSED | setup | src/index.js:190:55:190:77 | // test ... handler |
| PASSED | setup | src/index.js:192:39:192:61 | // test ... handler |
| PASSED | setup | src/index.js:194:49:194:71 | // test ... handler |
| PASSED | setup | src/index.js:198:65:198:87 | // test ... handler |
| PASSED | source | src/index.js:11:76:11:130 | // test ... k, !xss |
| PASSED | source | src/index.js:24:76:24:128 | // test ... nk, xss |
| PASSED | source | src/index.js:72:41:72:80 | // test ... reSink |
| PASSED | source | src/index.js:73:44:73:82 | // test ... ureSink |
| PASSED | source | src/index.js:74:40:74:78 | // test ... ureSink |
| PASSED | source | src/index.js:75:39:75:77 | // test ... ureSink |
| PASSED | source | src/index.js:78:32:78:60 | // test ... ectSink |
| PASSED | source | src/index.js:87:45:87:73 | // test ... ectSink |
| PASSED | source | src/index.js:88:40:88:68 | // test ... ectSink |
| PASSED | source | src/index.js:100:37:100:51 | // test: source |
| PASSED | source | src/index.js:123:40:123:92 | // test ... nk, xss |
| PASSED | source | src/index.js:129:31:129:45 | // test: source |
| PASSED | source | src/index.js:144:43:144:96 | // test ... k, !xss |
| PASSED | source | src/index.js:158:41:158:93 | // test ... nk, xss |
| PASSED | source | src/index.js:160:26:160:50 | // test ... srfSink |
| PASSED | source | src/index.js:162:45:162:69 | // test ... srfSink |
| PASSED | source | src/index.js:196:28:196:58 | // test ... k, !xss |
| PASSED | source | src/index.js:200:28:200:56 | // test ... nk, xss |
| PASSED | ssrfSink | src/index.js:160:26:160:50 | // test ... srfSink |
| PASSED | ssrfSink | src/index.js:162:45:162:69 | // test ... srfSink |
| PASSED | stackTraceExposureSink | src/index.js:9:66:9:96 | // test ... ureSink |
| PASSED | stackTraceExposureSink | src/index.js:11:76:11:130 | // test ... k, !xss |
| PASSED | stackTraceExposureSink | src/index.js:22:66:22:105 | // test ... xssSink |
| PASSED | stackTraceExposureSink | src/index.js:24:76:24:128 | // test ... nk, xss |
| PASSED | stackTraceExposureSink | src/index.js:72:41:72:80 | // test ... reSink |
| PASSED | stackTraceExposureSink | src/index.js:73:44:73:82 | // test ... ureSink |
| PASSED | stackTraceExposureSink | src/index.js:74:40:74:78 | // test ... ureSink |
| PASSED | stackTraceExposureSink | src/index.js:75:39:75:77 | // test ... ureSink |
| PASSED | stackTraceExposureSink | src/index.js:105:29:105:73 | // test ... nk, xss |
| PASSED | stackTraceExposureSink | src/index.js:116:29:116:59 | // test ... ureSink |
| PASSED | stackTraceExposureSink | src/index.js:123:40:123:92 | // test ... nk, xss |
| PASSED | stackTraceExposureSink | src/index.js:134:20:134:64 | // test ... nk, xss |
| PASSED | stackTraceExposureSink | src/index.js:144:43:144:96 | // test ... k, !xss |
| PASSED | stackTraceExposureSink | src/index.js:158:41:158:93 | // test ... nk, xss |
| PASSED | xss | src/index.js:24:76:24:128 | // test ... nk, xss |
| PASSED | xss | src/index.js:105:29:105:73 | // test ... nk, xss |
| PASSED | xss | src/index.js:123:40:123:92 | // test ... nk, xss |
| PASSED | xss | src/index.js:134:20:134:64 | // test ... nk, xss |
| PASSED | xss | src/index.js:158:41:158:93 | // test ... nk, xss |
| PASSED | xss | src/index.js:200:28:200:56 | // test ... nk, xss |
| PASSED | xssSink | src/index.js:22:66:22:105 | // test ... xssSink |
| PASSED | xssSink | src/index.js:24:76:24:128 | // test ... nk, xss |
| PASSED | xssSink | src/index.js:105:29:105:73 | // test ... nk, xss |
| PASSED | xssSink | src/index.js:123:40:123:92 | // test ... nk, xss |
| PASSED | xssSink | src/index.js:134:20:134:64 | // test ... nk, xss |
| PASSED | xssSink | src/index.js:144:43:144:96 | // test ... k, !xss |
| PASSED | xssSink | src/index.js:158:41:158:93 | // test ... nk, xss |
| PASSED | xssSink | src/index.js:200:28:200:56 | // test ... nk, xss |
failingPositiveTests
passingNegativeTests
| PASSED | !xss | src/index.js:11:76:11:130 | // test ... k, !xss |
| PASSED | !xss | src/index.js:144:43:144:96 | // test ... k, !xss |
| PASSED | !xss | src/index.js:196:28:196:58 | // test ... k, !xss |
| PASSED | !xssSink | src/index.js:11:76:11:130 | // test ... k, !xss |
| PASSED | !xssSink | src/index.js:196:28:196:58 | // test ... k, !xss |
failingNegativeTests

View File

@@ -0,0 +1,194 @@
import javascript
import semmle.javascript.security.dataflow.CleartextStorageCustomizations
import semmle.javascript.security.dataflow.CorsMisconfigurationForCredentialsCustomizations
import semmle.javascript.security.dataflow.StackTraceExposureCustomizations
import semmle.javascript.security.dataflow.ServerSideUrlRedirectCustomizations
import semmle.javascript.security.dataflow.RequestForgeryCustomizations
import semmle.javascript.security.dataflow.ReflectedXssCustomizations
import semmle.javascript.security.dataflow.ReflectedXssQuery as XssConfig
import semmle.javascript.heuristics.AdditionalRouteHandlers
class InlineTest extends LineComment {
string tests;
InlineTest() { tests = this.getText().regexpCapture("\\s*test:(.*)", 1) }
string getPositiveTest() {
result = tests.trim().splitAt(",").trim() and not result.matches("!%")
}
string getNegativeTest() { result = tests.trim().splitAt(",").trim() and result.matches("!%") }
predicate hasPositiveTest(string test) { test = this.getPositiveTest() }
predicate hasNegativeTest(string test) { test = this.getNegativeTest() }
predicate inNode(DataFlow::Node n) {
this.getLocation().getFile() = n.getFile() and
this.getLocation().getStartLine() = n.getStartLine()
}
}
query predicate passingPositiveTests(string res, string expectation, InlineTest t) {
res = "PASSED" and
t.hasPositiveTest(expectation) and
(
expectation = "source" and
exists(RemoteFlowSource n | t.inNode(n))
or
expectation = "setup" and
exists(Http::RouteSetup n | t.inNode(n))
or
expectation = "handler" and
exists(Http::RouteHandler n | t.inNode(n))
or
expectation = "candidateHandler" and
exists(Http::RouteHandlerCandidate n | t.inNode(n))
or
expectation = "xssSink" and
exists(ReflectedXss::Sink n | t.inNode(n))
or
expectation = "xss" and
exists(XssConfig::Configuration cfg, DataFlow::Node sink |
cfg.hasFlow(_, sink) and t.inNode(sink)
)
or
expectation = "cleartextStorageSink" and
exists(CleartextStorage::Sink n | t.inNode(n))
or
expectation = "corsMiconfigurationSink" and
exists(CorsMisconfigurationForCredentials::Sink n | t.inNode(n))
or
expectation = "stackTraceExposureSink" and
exists(StackTraceExposure::Sink n | t.inNode(n))
or
expectation = "redirectSink" and
exists(ServerSideUrlRedirect::Sink n | t.inNode(n))
or
expectation = "ssrfSink" and
exists(RequestForgery::Sink n | t.inNode(n))
)
}
query predicate failingPositiveTests(string res, string expectation, InlineTest t) {
res = "FAILED" and
t.hasPositiveTest(expectation) and
(
expectation = "source" and
not exists(RemoteFlowSource n | t.inNode(n))
or
expectation = "setup" and
not exists(Http::RouteSetup n | t.inNode(n))
or
expectation = "handler" and
not exists(Http::RouteHandler n | t.inNode(n))
or
expectation = "candidateHandler" and
not exists(Http::RouteHandlerCandidate n | t.inNode(n))
or
expectation = "xssSink" and
not exists(ReflectedXss::Sink n | t.inNode(n))
or
expectation = "xss" and
not exists(XssConfig::Configuration cfg, DataFlow::Node sink |
cfg.hasFlow(_, sink) and t.inNode(sink)
)
or
expectation = "cleartextStorageSink" and
not exists(CleartextStorage::Sink n | t.inNode(n))
or
expectation = "corsMiconfigurationSink" and
not exists(CorsMisconfigurationForCredentials::Sink n | t.inNode(n))
or
expectation = "stackTraceExposureSink" and
not exists(StackTraceExposure::Sink n | t.inNode(n))
or
expectation = "redirectSink" and
not exists(ServerSideUrlRedirect::Sink n | t.inNode(n))
or
expectation = "ssrfSink" and
not exists(RequestForgery::Sink n | t.inNode(n))
)
}
query predicate passingNegativeTests(string res, string expectation, InlineTest t) {
res = "PASSED" and
t.hasNegativeTest(expectation) and
(
expectation = "!source" and
not exists(RemoteFlowSource n | t.inNode(n))
or
expectation = "!setup" and
not exists(Http::RouteSetup n | t.inNode(n))
or
expectation = "!handler" and
not exists(Http::RouteHandler n | t.inNode(n))
or
expectation = "!candidateHandler" and
not exists(Http::RouteHandlerCandidate n | t.inNode(n))
or
expectation = "!xssSink" and
not exists(ReflectedXss::Sink n | t.inNode(n))
or
expectation = "!xss" and
not exists(XssConfig::Configuration cfg, DataFlow::Node sink |
cfg.hasFlow(_, sink) and t.inNode(sink)
)
or
expectation = "!cleartextStorageSink" and
not exists(CleartextStorage::Sink n | t.inNode(n))
or
expectation = "!corsMiconfigurationSink" and
not exists(CorsMisconfigurationForCredentials::Sink n | t.inNode(n))
or
expectation = "!stackTraceExposureSink" and
not exists(StackTraceExposure::Sink n | t.inNode(n))
or
expectation = "!redirectSink" and
not exists(ServerSideUrlRedirect::Sink n | t.inNode(n))
or
expectation = "!ssrfSink" and
not exists(RequestForgery::Sink n | t.inNode(n))
)
}
query predicate failingNegativeTests(string res, string expectation, InlineTest t) {
res = "FAILED" and
t.hasNegativeTest(expectation) and
(
expectation = "!source" and
exists(RemoteFlowSource n | t.inNode(n))
or
expectation = "!setup" and
exists(Http::RouteSetup n | t.inNode(n))
or
expectation = "!handler" and
exists(Http::RouteHandler n | t.inNode(n))
or
expectation = "!candidateHandler" and
exists(Http::RouteHandlerCandidate n | t.inNode(n))
or
expectation = "!xssSink" and
exists(ReflectedXss::Sink n | t.inNode(n))
or
expectation = "!xss" and
exists(XssConfig::Configuration cfg, DataFlow::Node sink |
cfg.hasFlow(_, sink) and t.inNode(sink)
)
or
expectation = "!cleartextStorageSink" and
exists(CleartextStorage::Sink n | t.inNode(n))
or
expectation = "!corsMiconfigurationSink" and
exists(CorsMisconfigurationForCredentials::Sink n | t.inNode(n))
or
expectation = "!stackTraceExposureSink" and
exists(StackTraceExposure::Sink n | t.inNode(n))
or
expectation = "!redirectSink" and
exists(ServerSideUrlRedirect::Sink n | t.inNode(n))
or
expectation = "!ssrfSink" and
exists(RequestForgery::Sink n | t.inNode(n))
)
}

View File

@@ -0,0 +1,17 @@
'use strict'
const routes = require('@npm/spife/routing')
module.exports = routes`
GET / homepage
GET /test1 test1
GET /test1 test2
GET /test4 test4
GET /test5 test5
GET /test6 test6
GET /raw1 raw1
GET /raw2 raw2
POST /body parseBody
GET /redirect/:redirect_url redirect
POST /packages/new createPackage
`(require('../views'))

View File

@@ -0,0 +1,46 @@
'use strict'
const { Environment, FileSystemLoader } = require('nunjucks')
const Loader = require('@npm/spife/templates/loader')
const path = require('path')
const isDev = !new Set(['prod', 'production', 'stag', 'staging']).has(
process.env.NODE_ENV
)
const templateDirs = [path.join(__dirname, '..', 'templates')]
const nunjucksEnv = new Environment(new FileSystemLoader(templateDirs))
const nunjucksLoader = new Loader({
dirs: templateDirs,
load (resolved) {
const template = nunjucksEnv.getTemplate(resolved.path, true)
return context => {
return template.render(context)
}
}
})
module.exports = {
DEBUG: process.env.DEBUG,
ENABLE_FORM_PARSING: false,
METRICS: process.env.METRICS,
MIDDLEWARE: [
'@npm/spife/middleware/debug',
['@npm/spife/middleware/template', [
nunjucksLoader
], [
// template context processors go here
]],
'@npm/spife/middleware/common',
'@npm/spife/middleware/logging',
'@npm/spife/middleware/metrics',
'@npm/spife/middleware/monitor',
'@npm/spife/middleware/hot-reload',
['@npm/spife/middleware/csrf', { secureCookie: !isDev }]
],
NAME: 'nunjucks-example',
NODE_ENV: process.env.NODE_ENV,
PORT: 8124,
ROUTER: './routes/index.js',
HOT: true
}

View File

@@ -0,0 +1,115 @@
'use strict'
const reply = require('@npm/spife/reply')
const validate = require('@npm/spife/decorators/validate')
const joi = require('@npm/spife/joi')
const concat = require('concat-stream')
const createPackageSchema = joi.object().keys({
contents: joi.string().max(200).required(),
destination: joi.any().valid([
joi.object({
name: joi.string().max(200).required(),
address: joi.string().max(200).required()
}),
joi.string().min(1)
])
})
module.exports = { homepage, parseBody, raw1, raw2, test1, test2, test3, test4, test5, test6, redirect, createPackage: validate.body(createPackageSchema, createPackage), }
function sink(obj) { console.log(obj) }
function createPackage(req, context) { // test: handler
const tainted = req.validatedBody.get('destination') // test: source
sink(taitned)
}
function homepage(req, context) { // test: handler
sink(req.cookie("test")) // test: source
sink(req.cookies().test) // test: source
sink(req.headers.test) // test: source
sink(req.rawHeaders[0]) // test: source
sink(req.raw.headers) // test: source
sink(req.url) // test: source
sink(req.urlObject.pathname) // test: source
sink(context.get('package')) // test: source
sink(context)
return reply.template('home', { target: req.query.name }) // test: source, templateInstantiation, stackTraceExposureSink
}
function raw1(req, context) { // test: handler
sink(req.query.name) // test: source
return reply(req.query.name, 200, { // test: source, xssSink, stackTraceExposureSink, xss
"content-type": "text/html",
"access-control-allow-origin": "*", // test: corsMiconfigurationSink
"access-control-allow-headers": "Content-Type, Authorization, Content-Length, X-Requested-With",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-credentials": "true"
})
}
function redirect(req, context) { // test: handler
return reply.redirect(context.get('redirect_url')) // test: redirectSink, source, stackTraceExposureSink
}
function raw2(req, context) { // test: handler
return reply.cookie({ "test": req.query.name }, "test", req.query.name, { "httpOnly": false, "secure": false }) // test: source, cleartextStorageSink, stackTraceExposureSink
}
function test1(req, context) { // test: handler
switch (req.accept.type(['json', 'html', 'plain'])) {
case 'json':
return { "some": req.query.name } // test: source, stackTraceExposureSink
case 'html':
return reply.header('<p>' + req.query.name + '</p>', 'content-type', 'text/html') // test: source, xssSink, stackTraceExposureSink, xss
case 'plain':
return reply.header('<p>' + req.query.name + '</p>', { 'content-type': 'text/plain' }) // test: source, stackTraceExposureSink, !xssSink, !xss
}
return 'well, I guess you just want plaintext.'
}
function test2(req, context) { // test: handler
switch (req.accept.type(['json', 'html'])) {
case 'json':
return { "some": req.query.name } // test: source, stackTraceExposureSink
case 'html':
return reply.header('<p>' + req.query.name + '</p>', { 'content-type': 'text/plain' }) // test: source, stackTraceExposureSink, !xssSink, !xss
}
return 'well, I guess you just want plaintext.'
}
function test3(req, context) { // test: candidateHandler
return reply('<p>' + req.query.name + '</p>') // test: source, stackTraceExposureSink, !xssSink, !xss
}
function test4(req, context) { // test: handler
const body = req.body // test: source
const newPackument = body['package-json']
const message = `INFO: User invited to package ${newPackument._id} successfully.`
return reply(message, 200, { 'npm-notice': message }) // test: stackTraceExposureSink, !xssSink, !xss
}
function test5(req, context) { // test: handler
const body = req.body // test: source
const newPackument = body['package-json']
const message = `INFO: User invited to package ${newPackument._id} successfully.`
return reply(message, 200) // test: stackTraceExposureSink, !xssSink, !xss
}
function test6(req, context) { // test: handler
const body = req.body // test: source
const newPackument = body['package-json']
const message = `INFO: User invited to package ${newPackument._id} successfully.`
if (message.contains('foo')) {
return reply(message, 200, { 'npm-notice': message }) // test: stackTraceExposureSink, !xssSink, !xss
} else {
return reply(message, 200, { 'npm-notice': message, 'content-type': 'text/html' }) // test: stackTraceExposureSink, xssSink, xss
}
}
function parseBody(req, context) {
return req.body.then(data => { // test: source, stackTraceExposureSink
sink(data.name)
})
}

View File

@@ -0,0 +1,75 @@
passingPositiveTests
| PASSED | candidateHandler | lib/views/index.js:82:32:82:56 | // test ... Handler |
| PASSED | cleartextStorageSink | lib/views/index.js:57:115:57:175 | // test ... ureSink |
| PASSED | corsMiconfigurationSink | lib/views/index.js:45:41:45:72 | // test ... ionSink |
| PASSED | handler | lib/views/index.js:23:40:23:55 | // test: handler |
| PASSED | handler | lib/views/index.js:28:35:28:50 | // test: handler |
| PASSED | handler | lib/views/index.js:41:31:41:46 | // test: handler |
| PASSED | handler | lib/views/index.js:53:35:53:50 | // test: handler |
| PASSED | handler | lib/views/index.js:56:31:56:46 | // test: handler |
| PASSED | handler | lib/views/index.js:60:32:60:47 | // test: handler |
| PASSED | handler | lib/views/index.js:72:32:72:47 | // test: handler |
| PASSED | handler | lib/views/index.js:86:32:86:47 | // test: handler |
| PASSED | handler | lib/views/index.js:93:32:93:47 | // test: handler |
| PASSED | handler | lib/views/index.js:100:32:100:47 | // test: handler |
| PASSED | redirectSink | lib/views/index.js:54:54:54:106 | // test ... ureSink |
| PASSED | source | lib/views/index.js:24:56:24:70 | // test: source |
| PASSED | source | lib/views/index.js:29:28:29:42 | // test: source |
| PASSED | source | lib/views/index.js:30:28:30:42 | // test: source |
| PASSED | source | lib/views/index.js:31:26:31:40 | // test: source |
| PASSED | source | lib/views/index.js:32:27:32:41 | // test: source |
| PASSED | source | lib/views/index.js:33:25:33:39 | // test: source |
| PASSED | source | lib/views/index.js:34:17:34:31 | // test: source |
| PASSED | source | lib/views/index.js:35:32:35:46 | // test: source |
| PASSED | source | lib/views/index.js:36:32:36:46 | // test: source |
| PASSED | source | lib/views/index.js:38:61:38:122 | // test ... ureSink |
| PASSED | source | lib/views/index.js:42:24:42:38 | // test: source |
| PASSED | source | lib/views/index.js:43:39:43:91 | // test ... nk, xss |
| PASSED | source | lib/views/index.js:54:54:54:106 | // test ... ureSink |
| PASSED | source | lib/views/index.js:57:115:57:175 | // test ... ureSink |
| PASSED | source | lib/views/index.js:63:41:63:79 | // test ... ureSink |
| PASSED | source | lib/views/index.js:65:89:65:141 | // test ... nk, xss |
| PASSED | source | lib/views/index.js:67:94:67:148 | // test ... k, !xss |
| PASSED | source | lib/views/index.js:75:41:75:79 | // test ... ureSink |
| PASSED | source | lib/views/index.js:77:94:77:148 | // test ... k, !xss |
| PASSED | source | lib/views/index.js:83:49:83:103 | // test ... k, !xss |
| PASSED | source | lib/views/index.js:87:25:87:39 | // test: source |
| PASSED | source | lib/views/index.js:94:25:94:39 | // test: source |
| PASSED | source | lib/views/index.js:101:25:101:39 | // test: source |
| PASSED | source | lib/views/index.js:112:34:112:72 | // test ... ureSink |
| PASSED | stackTraceExposureSink | lib/views/index.js:38:61:38:122 | // test ... ureSink |
| PASSED | stackTraceExposureSink | lib/views/index.js:43:39:43:91 | // test ... nk, xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:54:54:54:106 | // test ... ureSink |
| PASSED | stackTraceExposureSink | lib/views/index.js:57:115:57:175 | // test ... ureSink |
| PASSED | stackTraceExposureSink | lib/views/index.js:63:41:63:79 | // test ... ureSink |
| PASSED | stackTraceExposureSink | lib/views/index.js:65:89:65:141 | // test ... nk, xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:67:94:67:148 | // test ... k, !xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:75:41:75:79 | // test ... ureSink |
| PASSED | stackTraceExposureSink | lib/views/index.js:77:94:77:148 | // test ... k, !xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:83:49:83:103 | // test ... k, !xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:90:57:90:103 | // test ... k, !xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:97:30:97:76 | // test ... k, !xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:105:59:105:105 | // test ... k, !xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:107:88:107:132 | // test ... nk, xss |
| PASSED | stackTraceExposureSink | lib/views/index.js:112:34:112:72 | // test ... ureSink |
| PASSED | xss | lib/views/index.js:43:39:43:91 | // test ... nk, xss |
| PASSED | xss | lib/views/index.js:65:89:65:141 | // test ... nk, xss |
| PASSED | xss | lib/views/index.js:107:88:107:132 | // test ... nk, xss |
| PASSED | xssSink | lib/views/index.js:43:39:43:91 | // test ... nk, xss |
| PASSED | xssSink | lib/views/index.js:65:89:65:141 | // test ... nk, xss |
| PASSED | xssSink | lib/views/index.js:107:88:107:132 | // test ... nk, xss |
failingPositiveTests
passingNegativeTests
| PASSED | !xss | lib/views/index.js:67:94:67:148 | // test ... k, !xss |
| PASSED | !xss | lib/views/index.js:77:94:77:148 | // test ... k, !xss |
| PASSED | !xss | lib/views/index.js:83:49:83:103 | // test ... k, !xss |
| PASSED | !xss | lib/views/index.js:97:30:97:76 | // test ... k, !xss |
| PASSED | !xssSink | lib/views/index.js:67:94:67:148 | // test ... k, !xss |
| PASSED | !xssSink | lib/views/index.js:77:94:77:148 | // test ... k, !xss |
| PASSED | !xssSink | lib/views/index.js:83:49:83:103 | // test ... k, !xss |
| PASSED | !xssSink | lib/views/index.js:97:30:97:76 | // test ... k, !xss |
failingNegativeTests
| FAILED | !xss | lib/views/index.js:90:57:90:103 | // test ... k, !xss |
| FAILED | !xss | lib/views/index.js:105:59:105:105 | // test ... k, !xss |
| FAILED | !xssSink | lib/views/index.js:90:57:90:103 | // test ... k, !xss |
| FAILED | !xssSink | lib/views/index.js:105:59:105:105 | // test ... k, !xss |

View File

@@ -0,0 +1,194 @@
import javascript
import semmle.javascript.security.dataflow.CleartextStorageCustomizations
import semmle.javascript.security.dataflow.CorsMisconfigurationForCredentialsCustomizations
import semmle.javascript.security.dataflow.StackTraceExposureCustomizations
import semmle.javascript.security.dataflow.ServerSideUrlRedirectCustomizations
import semmle.javascript.security.dataflow.RequestForgeryCustomizations
import semmle.javascript.security.dataflow.ReflectedXssCustomizations
import semmle.javascript.security.dataflow.ReflectedXssQuery as XssConfig
import semmle.javascript.heuristics.AdditionalRouteHandlers
class InlineTest extends LineComment {
string tests;
InlineTest() { tests = this.getText().regexpCapture("\\s*test:(.*)", 1) }
string getPositiveTest() {
result = tests.trim().splitAt(",").trim() and not result.matches("!%")
}
string getNegativeTest() { result = tests.trim().splitAt(",").trim() and result.matches("!%") }
predicate hasPositiveTest(string test) { test = this.getPositiveTest() }
predicate hasNegativeTest(string test) { test = this.getNegativeTest() }
predicate inNode(DataFlow::Node n) {
this.getLocation().getFile() = n.getFile() and
this.getLocation().getStartLine() = n.getStartLine()
}
}
query predicate passingPositiveTests(string res, string expectation, InlineTest t) {
res = "PASSED" and
t.hasPositiveTest(expectation) and
(
expectation = "source" and
exists(RemoteFlowSource n | t.inNode(n))
or
expectation = "setup" and
exists(Http::RouteSetup n | t.inNode(n))
or
expectation = "handler" and
exists(Http::RouteHandler n | t.inNode(n))
or
expectation = "candidateHandler" and
exists(Http::RouteHandlerCandidate n | t.inNode(n))
or
expectation = "xssSink" and
exists(ReflectedXss::Sink n | t.inNode(n))
or
expectation = "xss" and
exists(XssConfig::Configuration cfg, DataFlow::Node sink |
cfg.hasFlow(_, sink) and t.inNode(sink)
)
or
expectation = "cleartextStorageSink" and
exists(CleartextStorage::Sink n | t.inNode(n))
or
expectation = "corsMiconfigurationSink" and
exists(CorsMisconfigurationForCredentials::Sink n | t.inNode(n))
or
expectation = "stackTraceExposureSink" and
exists(StackTraceExposure::Sink n | t.inNode(n))
or
expectation = "redirectSink" and
exists(ServerSideUrlRedirect::Sink n | t.inNode(n))
or
expectation = "ssrfSink" and
exists(RequestForgery::Sink n | t.inNode(n))
)
}
query predicate failingPositiveTests(string res, string expectation, InlineTest t) {
res = "FAILED" and
t.hasPositiveTest(expectation) and
(
expectation = "source" and
not exists(RemoteFlowSource n | t.inNode(n))
or
expectation = "setup" and
not exists(Http::RouteSetup n | t.inNode(n))
or
expectation = "handler" and
not exists(Http::RouteHandler n | t.inNode(n))
or
expectation = "candidateHandler" and
not exists(Http::RouteHandlerCandidate n | t.inNode(n))
or
expectation = "xssSink" and
not exists(ReflectedXss::Sink n | t.inNode(n))
or
expectation = "xss" and
not exists(XssConfig::Configuration cfg, DataFlow::Node sink |
cfg.hasFlow(_, sink) and t.inNode(sink)
)
or
expectation = "cleartextStorageSink" and
not exists(CleartextStorage::Sink n | t.inNode(n))
or
expectation = "corsMiconfigurationSink" and
not exists(CorsMisconfigurationForCredentials::Sink n | t.inNode(n))
or
expectation = "stackTraceExposureSink" and
not exists(StackTraceExposure::Sink n | t.inNode(n))
or
expectation = "redirectSink" and
not exists(ServerSideUrlRedirect::Sink n | t.inNode(n))
or
expectation = "ssrfSink" and
not exists(RequestForgery::Sink n | t.inNode(n))
)
}
query predicate passingNegativeTests(string res, string expectation, InlineTest t) {
res = "PASSED" and
t.hasNegativeTest(expectation) and
(
expectation = "!source" and
not exists(RemoteFlowSource n | t.inNode(n))
or
expectation = "!setup" and
not exists(Http::RouteSetup n | t.inNode(n))
or
expectation = "!handler" and
not exists(Http::RouteHandler n | t.inNode(n))
or
expectation = "!candidateHandler" and
not exists(Http::RouteHandlerCandidate n | t.inNode(n))
or
expectation = "!xssSink" and
not exists(ReflectedXss::Sink n | t.inNode(n))
or
expectation = "!xss" and
not exists(XssConfig::Configuration cfg, DataFlow::Node sink |
cfg.hasFlow(_, sink) and t.inNode(sink)
)
or
expectation = "!cleartextStorageSink" and
not exists(CleartextStorage::Sink n | t.inNode(n))
or
expectation = "!corsMiconfigurationSink" and
not exists(CorsMisconfigurationForCredentials::Sink n | t.inNode(n))
or
expectation = "!stackTraceExposureSink" and
not exists(StackTraceExposure::Sink n | t.inNode(n))
or
expectation = "!redirectSink" and
not exists(ServerSideUrlRedirect::Sink n | t.inNode(n))
or
expectation = "!ssrfSink" and
not exists(RequestForgery::Sink n | t.inNode(n))
)
}
query predicate failingNegativeTests(string res, string expectation, InlineTest t) {
res = "FAILED" and
t.hasNegativeTest(expectation) and
(
expectation = "!source" and
exists(RemoteFlowSource n | t.inNode(n))
or
expectation = "!setup" and
exists(Http::RouteSetup n | t.inNode(n))
or
expectation = "!handler" and
exists(Http::RouteHandler n | t.inNode(n))
or
expectation = "!candidateHandler" and
exists(Http::RouteHandlerCandidate n | t.inNode(n))
or
expectation = "!xssSink" and
exists(ReflectedXss::Sink n | t.inNode(n))
or
expectation = "!xss" and
exists(XssConfig::Configuration cfg, DataFlow::Node sink |
cfg.hasFlow(_, sink) and t.inNode(sink)
)
or
expectation = "!cleartextStorageSink" and
exists(CleartextStorage::Sink n | t.inNode(n))
or
expectation = "!corsMiconfigurationSink" and
exists(CorsMisconfigurationForCredentials::Sink n | t.inNode(n))
or
expectation = "!stackTraceExposureSink" and
exists(StackTraceExposure::Sink n | t.inNode(n))
or
expectation = "!redirectSink" and
exists(ServerSideUrlRedirect::Sink n | t.inNode(n))
or
expectation = "!ssrfSink" and
exists(RequestForgery::Sink n | t.inNode(n))
)
}

View File

@@ -11,10 +11,16 @@ test_RequestInputAccess
| src/test.js:19:5:19:26 | request ... ('bar') | header | src/test.js:12:19:22:1 | functio ... okie;\\n} |
| src/test.js:20:5:20:25 | request ... ('baz') | header | src/test.js:12:19:22:1 | functio ... okie;\\n} |
test_RouteHandler_getAResponseHeader
| src/test.js:6:1:6:21 | functio ... er1(){} | content-type | src/test.js:6:1:6:21 | functio ... er1(){} |
| src/test.js:9:19:11:1 | functio ... ition\\n} | content-type | src/test.js:9:19:11:1 | functio ... ition\\n} |
| src/test.js:9:19:11:1 | functio ... ition\\n} | header1 | src/test.js:10:5:10:34 | respons ... 1', '') |
| src/test.js:12:19:22:1 | functio ... okie;\\n} | content-type | src/test.js:12:19:22:1 | functio ... okie;\\n} |
| src/test.js:12:19:22:1 | functio ... okie;\\n} | header2 | src/test.js:13:5:13:37 | respons ... 2', '') |
test_HeaderDefinition_defines
| src/test.js:6:1:6:21 | functio ... er1(){} | content-type | application/json |
| src/test.js:9:19:11:1 | functio ... ition\\n} | content-type | application/json |
| src/test.js:10:5:10:34 | respons ... 1', '') | header1 | |
| src/test.js:12:19:22:1 | functio ... okie;\\n} | content-type | application/json |
| src/test.js:13:5:13:37 | respons ... 2', '') | header2 | |
test_ResponseExpr
| src/test.js:9:46:9:53 | response | src/test.js:9:19:11:1 | functio ... ition\\n} |
@@ -24,14 +30,20 @@ test_ResponseExpr
| src/test.js:12:46:12:53 | response | src/test.js:12:19:22:1 | functio ... okie;\\n} |
| src/test.js:13:5:13:12 | response | src/test.js:12:19:22:1 | functio ... okie;\\n} |
test_HeaderDefinition
| src/test.js:6:1:6:21 | functio ... er1(){} | src/test.js:6:1:6:21 | functio ... er1(){} |
| src/test.js:9:19:11:1 | functio ... ition\\n} | src/test.js:9:19:11:1 | functio ... ition\\n} |
| src/test.js:10:5:10:34 | respons ... 1', '') | src/test.js:9:19:11:1 | functio ... ition\\n} |
| src/test.js:12:19:22:1 | functio ... okie;\\n} | src/test.js:12:19:22:1 | functio ... okie;\\n} |
| src/test.js:13:5:13:37 | respons ... 2', '') | src/test.js:12:19:22:1 | functio ... okie;\\n} |
test_RouteSetup_getServer
| src/test.js:7:1:7:26 | server2 ... ndler1) | src/test.js:4:15:4:36 | restify ... erver() |
| src/test.js:9:1:11:2 | server2 ... tion\\n}) | src/test.js:4:15:4:36 | restify ... erver() |
| src/test.js:12:1:22:2 | server2 ... kie;\\n}) | src/test.js:4:15:4:36 | restify ... erver() |
test_HeaderDefinition_getAHeaderName
| src/test.js:6:1:6:21 | functio ... er1(){} | content-type |
| src/test.js:9:19:11:1 | functio ... ition\\n} | content-type |
| src/test.js:10:5:10:34 | respons ... 1', '') | header1 |
| src/test.js:12:19:22:1 | functio ... okie;\\n} | content-type |
| src/test.js:13:5:13:37 | respons ... 2', '') | header2 |
test_ServerDefinition
| src/test.js:1:15:1:47 | require ... erver() |