diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll index d704a691..9c263246 100644 --- a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll @@ -1040,3 +1040,81 @@ class CdsQlCall extends CqlClauseParserCall { ) } } + +/** + * Exported functions from CAP `cds.utils`. + * Functions described from: + * https://www.npmjs.com/package/@sap/cds?activeTab=code + */ +module CdsUtils { + /** + * An access to the `utils` module on a CDS facade. + */ + class CdsUtilsModuleAccess extends API::Node { + CdsUtilsModuleAccess() { exists(CdsFacade cds | this = cds.getMember("utils")) } + + //additional flow steps + DataFlow::CallNode getThroughCall() { + result = + this.getMember(["decodeURI", "decodeURIComponent", "local", "isdir", "isfile"]).getACall() + } + + //sinks + DataFlow::CallNode getSingleArgCalls() { + result = + this.getMember(["find", "stat", "read", "readdir", "mkdirp", "rmdir", "rimraf", "rm"]) + .getACall() + } + + DataFlow::CallNode getCopyWriteCall() { + result = this.getMember(["append", "copy", "write"]).getACall() + } + } + + abstract class UtilsSink extends DataFlow::Node { } + + abstract class UtilsExtraFlow extends DataFlow::Node { } + + /** + * This represents both the data and the filename in calls as follows: + * ```javascript + * await write ({foo:'bar'}) .to ('some','file.json') + * ``` + * sinks in this example are: + * ```javascript + * {foo:'bar'} + * 'some' + * 'file.json' + * ``` + */ + class SrcDstSink extends UtilsSink { + SrcDstSink() { + this = copyWriteUtils().(DataFlow::CallNode).getAnArgument() or + this = copyWriteUtils().getAMemberCall("to").getAnArgument() + } + } + + /** + * This represents arguments to calls where the argument represents a path. e.g. + * ```javascript + * await rimraf('dist','db','data') + * ``` + */ + class SimpleSinks extends UtilsSink { + SimpleSinks() { this = singleArgCallsUtils().(DataFlow::CallNode).getAnArgument() } + } + + /** + * This represents calls where the taint flows through the call. e.g. + * ```javascript + * let dir = isdir ('app') + * ``` + */ + class SimpleAdditionalFlowStep extends UtilsExtraFlow { + SimpleAdditionalFlowStep() { this = singleArgAdditionalFlowUtils() } + + DataFlow::CallNode getOutgoingNode() { result = this } + + DataFlow::Node getIngoingNode() { result = this.(DataFlow::CallNode).getAnArgument() } + } +} diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/TypeTrackers.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/TypeTrackers.qll index f2f5af3f..f08492c2 100644 --- a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/TypeTrackers.qll +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/TypeTrackers.qll @@ -38,4 +38,33 @@ private SourceNode cdsApplicationServiceInstantiation(TypeTracker t) { SourceNode cdsApplicationServiceInstantiation() { result = cdsApplicationServiceInstantiation(TypeTracker::end()) -} \ No newline at end of file +} + +SourceNode copyWriteUtils(TypeTracker t) { + t.start() and + exists(CdsUtils::CdsUtilsModuleAccess mod | result = mod.getCopyWriteCall()) + or + exists(TypeTracker t2 | result = copyWriteUtils(t2).track(t2, t)) +} + +SourceNode copyWriteUtils() { result = copyWriteUtils(TypeTracker::end()) } + +SourceNode singleArgCallsUtils(TypeTracker t) { + t.start() and + exists(CdsUtils::CdsUtilsModuleAccess mod | result = mod.getSingleArgCalls()) + or + exists(TypeTracker t2 | result = singleArgCallsUtils(t2).track(t2, t)) +} + +SourceNode singleArgCallsUtils() { result = singleArgCallsUtils(TypeTracker::end()) } + +SourceNode singleArgAdditionalFlowUtils(TypeTracker t) { + t.start() and + exists(CdsUtils::CdsUtilsModuleAccess mod | result = mod.getThroughCall()) + or + exists(TypeTracker t2 | result = singleArgAdditionalFlowUtils(t2).track(t2, t)) +} + +SourceNode singleArgAdditionalFlowUtils() { + result = singleArgAdditionalFlowUtils(TypeTracker::end()) +} diff --git a/javascript/frameworks/cap/test/models/cds/utils/utils.expected b/javascript/frameworks/cap/test/models/cds/utils/utils.expected new file mode 100644 index 00000000..58679221 --- /dev/null +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.expected @@ -0,0 +1,37 @@ +| utils.js:5:11:5:31 | decodeU ... %A4%A") | decodeU ... %A4%A"): additional flow step | +| utils.js:7:12:7:41 | decodeU ... %A4%A") | decodeU ... %A4%A"): additional flow step | +| utils.js:9:12:9:28 | local("%E0%A4%A") | local("%E0%A4%A"): additional flow step | +| utils.js:13:11:13:22 | isdir('app') | isdir('app'): additional flow step | +| utils.js:15:12:15:33 | isfile( ... .json') | isfile( ... .json'): additional flow step | +| utils.js:17:22:17:35 | 'package.json' | 'package.json': sink | +| utils.js:19:26:19:39 | 'package.json' | 'package.json': sink | +| utils.js:21:20:21:33 | 'package.json' | 'package.json': sink | +| utils.js:23:20:23:33 | 'package.json' | 'package.json': sink | +| utils.js:25:14:25:22 | 'db/data' | 'db/data': sink | +| utils.js:25:28:25:41 | 'dist/db/data' | 'dist/db/data': sink | +| utils.js:26:14:26:22 | 'db/data' | 'db/data': sink | +| utils.js:26:25:26:38 | 'dist/db/data' | 'dist/db/data': sink | +| utils.js:28:12:28:20 | 'db/data' | 'db/data': sink | +| utils.js:28:26:28:39 | 'dist/db/data' | 'dist/db/data': sink | +| utils.js:29:12:29:20 | 'db/data' | 'db/data': sink | +| utils.js:29:23:29:36 | 'dist/db/data' | 'dist/db/data': sink | +| utils.js:31:13:31:26 | { foo: 'bar' } | { foo: 'bar' }: sink | +| utils.js:31:32:31:47 | 'some/file.json' | 'some/file.json': sink | +| utils.js:32:13:32:28 | 'some/file.json' | 'some/file.json': sink | +| utils.js:32:31:32:44 | { foo: 'bar' } | { foo: 'bar' }: sink | +| utils.js:34:14:34:19 | 'dist' | 'dist': sink | +| utils.js:34:22:34:25 | 'db' | 'db': sink | +| utils.js:34:28:34:33 | 'data' | 'data': sink | +| utils.js:35:14:35:27 | 'dist/db/data' | 'dist/db/data': sink | +| utils.js:37:13:37:18 | 'dist' | 'dist': sink | +| utils.js:37:21:37:24 | 'db' | 'db': sink | +| utils.js:37:27:37:32 | 'data' | 'data': sink | +| utils.js:38:13:38:26 | 'dist/db/data' | 'dist/db/data': sink | +| utils.js:40:14:40:19 | 'dist' | 'dist': sink | +| utils.js:40:22:40:25 | 'db' | 'db': sink | +| utils.js:40:28:40:33 | 'data' | 'data': sink | +| utils.js:41:14:41:27 | 'dist/db/data' | 'dist/db/data': sink | +| utils.js:43:10:43:15 | 'dist' | 'dist': sink | +| utils.js:43:18:43:21 | 'db' | 'db': sink | +| utils.js:43:24:43:29 | 'data' | 'data': sink | +| utils.js:44:10:44:23 | 'dist/db/data' | 'dist/db/data': sink | diff --git a/javascript/frameworks/cap/test/models/cds/utils/utils.js b/javascript/frameworks/cap/test/models/cds/utils/utils.js new file mode 100644 index 00000000..9420e474 --- /dev/null +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.js @@ -0,0 +1,44 @@ +const cds = require("@sap/cds"); + +const { decodeURI, decodeURIComponent, local, exists, isdir, isfile, read, readdir, append, write, copy, stat, find, mkdirp, rmdir, rimraf, rm } = cds.utils + +let uri = decodeURI("%E0%A4%A") // taint step + +let uri2 = decodeURIComponent("%E0%A4%A") // taint step + +let uri3 = local("%E0%A4%A") // taint step + +let uri4 = exists("%E0%A4%A") // NOT a taint step - returns a boolean + +let dir = isdir('app') // taint step + +let file = isfile('package.json') // taint step + +let pkg = await read('package.json') // sink + +let pdir = await readdir('package.json') // sink + +let s = await stat('package.json') // sink + +let f = await find('package.json') // sink + +await append('db/data').to('dist/db/data') // sink +await append('db/data', 'dist/db/data') // sink + +await copy('db/data').to('dist/db/data') // sink +await copy('db/data', 'dist/db/data') // sink + +await write({ foo: 'bar' }).to('some/file.json') // sink +await write('some/file.json', { foo: 'bar' }) // sink + +await mkdirp('dist', 'db', 'data') // sink +await mkdirp('dist/db/data') // sink + +await rmdir('dist', 'db', 'data') // sink +await rmdir('dist/db/data') // sink + +await rimraf('dist', 'db', 'data') // sink +await rimraf('dist/db/data') // sink + +await rm('dist', 'db', 'data') // sink +await rm('dist/db/data') // sink \ No newline at end of file diff --git a/javascript/frameworks/cap/test/models/cds/utils/utils.ql b/javascript/frameworks/cap/test/models/cds/utils/utils.ql new file mode 100644 index 00000000..ededca28 --- /dev/null +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.ql @@ -0,0 +1,9 @@ +import javascript +import advanced_security.javascript.frameworks.cap.CDS + +from DataFlow::Node node, string str, string strfull +where + node.(CdsUtils::UtilsSink).toString() = str and strfull = str + ": sink" + or + node.(CdsUtils::UtilsExtraFlow).toString() = str and strfull = str + ": additional flow step" +select node, strfull