diff --git a/change-notes/1.25/analysis-javascript.md b/change-notes/1.25/analysis-javascript.md index 2aada0cbd86..2b27c633659 100644 --- a/change-notes/1.25/analysis-javascript.md +++ b/change-notes/1.25/analysis-javascript.md @@ -11,6 +11,7 @@ - [jGrowl](https://github.com/stanlemon/jGrowl) - [jQuery](https://jquery.com/) - [marsdb](https://www.npmjs.com/package/marsdb) + - [micro](https://www.npmjs.com/package/micro/) - [minimongo](https://www.npmjs.com/package/minimongo/) - [mssql](https://www.npmjs.com/package/mssql) - [mysql](https://www.npmjs.com/package/mysql) diff --git a/javascript/ql/src/semmle/javascript/Promises.qll b/javascript/ql/src/semmle/javascript/Promises.qll index af636dfabe3..15e8695af5f 100644 --- a/javascript/ql/src/semmle/javascript/Promises.qll +++ b/javascript/ql/src/semmle/javascript/Promises.qll @@ -576,6 +576,22 @@ module Bluebird { override DataFlow::Node getArrayNode() { result = getArgument(0) } } + + /** + * An async function created using a call to `bluebird.coroutine`. + */ + class BluebirdCoroutineDefinition extends DataFlow::CallNode { + BluebirdCoroutineDefinition() { this = bluebird().getAMemberCall("coroutine") } + } + + private class BluebirdCoroutineDefinitionAsPartialInvoke extends DataFlow::PartialInvokeNode::Range, + BluebirdCoroutineDefinition { + override DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) { + boundArgs = 0 and + callback = getArgument(0) and + result = this + } + } } /** diff --git a/javascript/ql/src/semmle/javascript/frameworks/HttpFrameworks.qll b/javascript/ql/src/semmle/javascript/frameworks/HttpFrameworks.qll index c6798fb22a5..930d2a6f5b4 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/HttpFrameworks.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/HttpFrameworks.qll @@ -2,6 +2,7 @@ import semmle.javascript.frameworks.Express import semmle.javascript.frameworks.Hapi import semmle.javascript.frameworks.Koa import semmle.javascript.frameworks.NodeJSLib +import semmle.javascript.frameworks.Micro import semmle.javascript.frameworks.Restify import semmle.javascript.frameworks.Connect import semmle.javascript.frameworks.Fastify diff --git a/javascript/ql/src/semmle/javascript/frameworks/Micro.qll b/javascript/ql/src/semmle/javascript/frameworks/Micro.qll new file mode 100644 index 00000000000..40d7553a9c3 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/frameworks/Micro.qll @@ -0,0 +1,114 @@ +/** + * Provides a model of the `micro` NPM package. + */ + +private import javascript + +private module Micro { + private import DataFlow + + /** + * A node that should be interpreted as a route handler, to use as starting + * point for back-tracking. + */ + Node microRouteHandlerSink() { + result = moduleMember("micro", "run").getACall().getLastArgument() + or + result = moduleImport("micro").getACall().getArgument(0) + } + + /** Gets a data flow node interpreted as a route handler. */ + private DataFlow::SourceNode microRouteHandler(DataFlow::TypeBackTracker t) { + t.start() and + result = microRouteHandlerSink().getALocalSource() + or + exists(DataFlow::TypeBackTracker t2 | result = microRouteHandler(t2).backtrack(t2, t)) + or + exists(DataFlow::CallNode transformer | + transformer = moduleImport("micro-compress").getACall() + or + transformer instanceof Bluebird::BluebirdCoroutineDefinition + | + microRouteHandler(t.continue()) = transformer and + result = transformer.getArgument(0).getALocalSource() + ) + } + + /** Gets a data flow node interpreted as a route handler. */ + DataFlow::SourceNode microRouteHandler() { + result = microRouteHandler(DataFlow::TypeBackTracker::end()) + } + + /** + * A function passed to `micro` or `micro.run`. + */ + class MicroRouteHandler extends HTTP::Servers::StandardRouteHandler, DataFlow::FunctionNode { + MicroRouteHandler() { this = microRouteHandler().getAFunctionValue() } + } + + class MicroRequestSource extends HTTP::Servers::RequestSource { + MicroRouteHandler h; + + MicroRequestSource() { this = h.getParameter(0) } + + override HTTP::RouteHandler getRouteHandler() { result = h } + } + + class MicroResponseSource extends HTTP::Servers::ResponseSource { + MicroRouteHandler h; + + MicroResponseSource() { this = h.getParameter(1) } + + override HTTP::RouteHandler getRouteHandler() { result = h } + } + + class MicroRequestExpr extends NodeJSLib::RequestExpr { + override MicroRequestSource src; + } + + class MicroReseponseExpr extends NodeJSLib::ResponseExpr { + override MicroResponseSource src; + } + + private HTTP::RouteHandler getRouteHandlerFromReqRes(DataFlow::Node node) { + exists(HTTP::Servers::RequestSource src | + src.ref().flowsTo(node) and + result = src.getRouteHandler() + ) + or + exists(HTTP::Servers::ResponseSource src | + src.ref().flowsTo(node) and + result = src.getRouteHandler() + ) + } + + class MicroBodyParserCall extends HTTP::RequestInputAccess, DataFlow::CallNode { + string name; + + MicroBodyParserCall() { + name = ["buffer", "text", "json"] and + this = moduleMember("micro", name).getACall() + } + + override string getKind() { result = "body" } + + override HTTP::RouteHandler getRouteHandler() { + result = getRouteHandlerFromReqRes(getArgument(0)) + } + + override predicate isUserControlledObject() { name = "json" } + } + + class MicroSendArgument extends HTTP::ResponseSendArgument { + CallNode send; + + MicroSendArgument() { + send = moduleMember("micro", ["send", "sendError"]).getACall() and + this = send.getLastArgument().asExpr() + } + + override HTTP::RouteHandler getRouteHandler() { + result = getRouteHandlerFromReqRes(send.getArgument([0, 1])) + } + } +} diff --git a/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll b/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll index 9e33db18147..066b6788799 100644 --- a/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll +++ b/javascript/ql/src/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll @@ -212,11 +212,10 @@ module TaintedPath { DataFlow::Node output; PreservingPathCall() { - exists(string name | name = "dirname" or name = "toNamespacedPath" | - this = NodeJSLib::Path::moduleMember(name).getACall() and - input = getAnArgument() and - output = this - ) + this = + NodeJSLib::Path::moduleMember(["dirname", "toNamespacedPath", "parse", "format"]).getACall() and + input = getAnArgument() and + output = this or // non-global replace or replace of something other than /\.\./g, /[/]/g, or /[\.]/g. this.getCalleeName() = "replace" and diff --git a/javascript/ql/test/library-tests/frameworks/Micro/TestMicro.expected b/javascript/ql/test/library-tests/frameworks/Micro/TestMicro.expected new file mode 100644 index 00000000000..1a6c86bacf5 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Micro/TestMicro.expected @@ -0,0 +1,19 @@ +routeHandler +| tst.js:5:7:10:1 | async ( ... llo";\\n} | +| tst.js:12:1:15:1 | functio ... nse";\\n} | +requestSource +| tst.js:5:14:5:16 | req | +| tst.js:12:26:12:28 | req | +responseSource +| tst.js:5:19:5:21 | res | +| tst.js:12:31:12:33 | res | +requestInputAccess +| body | tst.js:7:5:7:19 | micro.json(req) | +| header | tst.js:6:5:6:31 | req.hea ... -type'] | +| header | tst.js:13:5:13:31 | req.hea ... -type'] | +userControlledObject +| tst.js:7:5:7:19 | micro.json(req) | +responseSendArgument +| tst.js:8:31:8:36 | "data" | +responseSendArgumentHandler +| tst.js:5:7:10:1 | async ( ... llo";\\n} | tst.js:8:31:8:36 | "data" | diff --git a/javascript/ql/test/library-tests/frameworks/Micro/TestMicro.ql b/javascript/ql/test/library-tests/frameworks/Micro/TestMicro.ql new file mode 100644 index 00000000000..97f4d7caf1e --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Micro/TestMicro.ql @@ -0,0 +1,17 @@ +import javascript + +query HTTP::RouteHandler routeHandler() { any() } + +query HTTP::Servers::RequestSource requestSource() { any() } + +query HTTP::Servers::ResponseSource responseSource() { any() } + +query HTTP::RequestInputAccess requestInputAccess(string kind) { kind = result.getKind() } + +query HTTP::RequestInputAccess userControlledObject() { result.isUserControlledObject() } + +query HTTP::ResponseSendArgument responseSendArgument() { any() } + +query HTTP::ResponseSendArgument responseSendArgumentHandler(HTTP::RouteHandler h) { + h = result.getRouteHandler() +} diff --git a/javascript/ql/test/library-tests/frameworks/Micro/tst.js b/javascript/ql/test/library-tests/frameworks/Micro/tst.js new file mode 100644 index 00000000000..539efe821fd --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Micro/tst.js @@ -0,0 +1,19 @@ +const micro = require('micro') +const bluebird = require('bluebird'); +const compress = require('micro-compress'); + +micro(async (req, res) => { + req.headers['content-type']; + micro.json(req); + micro.sendError(req, res, "data"); + return "Hello"; +}) + +function* wrappedHandler(req, res) { + req.headers['content-type']; + yield "Response"; +} + +let handler = bluebird.coroutine(wrappedHandler); + +micro(compress(handler));