From db2e27a048b90289f3c73bae71f7a071c28f4844 Mon Sep 17 00:00:00 2001 From: Kristen Newbury Date: Tue, 22 Jul 2025 15:15:29 -0400 Subject: [PATCH 1/6] Add cds utils modelling and test for model --- .../javascript/frameworks/cap/CDS.qll | 78 +++++++++++++++++++ .../frameworks/cap/TypeTrackers.qll | 31 +++++++- .../cap/test/models/cds/utils/utils.expected | 37 +++++++++ .../cap/test/models/cds/utils/utils.js | 44 +++++++++++ .../cap/test/models/cds/utils/utils.ql | 9 +++ 5 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 javascript/frameworks/cap/test/models/cds/utils/utils.expected create mode 100644 javascript/frameworks/cap/test/models/cds/utils/utils.js create mode 100644 javascript/frameworks/cap/test/models/cds/utils/utils.ql 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 d704a6910..9c263246b 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 f2f5af3f6..f08492c2b 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 000000000..586792217 --- /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 000000000..9420e4749 --- /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 000000000..ededca287 --- /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 From 56378ebff4a4b2e2d05abb0c9c85672c792a5aa6 Mon Sep 17 00:00:00 2001 From: Kristen Newbury Date: Mon, 11 Aug 2025 16:48:54 -0400 Subject: [PATCH 2/6] Refactor utils lib modelling --- .../frameworks/cap/CAPPathInjectionQuery.qll | 231 ++++++++++++++++++ .../javascript/frameworks/cap/CDS.qll | 78 ------ .../frameworks/cap/TypeTrackers.qll | 29 --- .../cap/test/models/cds/utils/utils.expected | 10 +- .../cap/test/models/cds/utils/utils.ql | 6 +- 5 files changed, 239 insertions(+), 115 deletions(-) create mode 100644 javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll new file mode 100644 index 000000000..0cdda7e00 --- /dev/null +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll @@ -0,0 +1,231 @@ +/** + * Exported functions from CAP `cds.utils`. + * Functions described from: + * https://www.npmjs.com/package/@sap/cds?activeTab=code + */ + +import javascript +import advanced_security.javascript.frameworks.cap.CDS + +/** + * An access to the `utils` module on a CDS facade. + */ +class CdsUtilsModuleAccess extends API::Node { + CdsUtilsModuleAccess() { exists(CdsFacade cds | this = cds.getMember("utils")) } +} + +class PathConverters extends DataFlow::Node { + PathConverters() { + exists(CdsUtilsModuleAccess utils | + utils.getMember(["decodeURI", "decodeURIComponent", "local"]).getACall() = this + ) + } + + SourceNode pathConvertersUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = pathConvertersUtils(t2).track(t2, t)) + } + + SourceNode pathConvertersUtils() { result = pathConvertersUtils(TypeTracker::end()) } + + DataFlow::Node getPath() { pathConvertersUtils().(DataFlow::CallNode).getAnArgument() = result } +} + +class PathPredicates extends DataFlow::Node { + PathPredicates() { + exists(CdsUtilsModuleAccess utils | utils.getMember(["isdir", "isfile"]).getACall() = this) + } + + SourceNode pathPredicateUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = pathPredicateUtils(t2).track(t2, t)) + } + + SourceNode pathPredicateUtils() { result = pathPredicateUtils(TypeTracker::end()) } + + DataFlow::Node getPath() { pathPredicateUtils().(DataFlow::CallNode).getAnArgument() = result } +} + +class DirectoryReaders extends DataFlow::Node { + DirectoryReaders() { + exists(CdsUtilsModuleAccess utils | + utils.getMember(["find", "stat", "readdir"]).getACall() = this + ) + } + + SourceNode directoryReaderUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = directoryReaderUtils(t2).track(t2, t)) + } + + SourceNode directoryReaderUtils() { result = directoryReaderUtils(TypeTracker::end()) } + + DataFlow::Node getPath() { directoryReaderUtils().(DataFlow::CallNode).getAnArgument() = result } +} + +class DirectoryWriters extends DataFlow::Node { + DirectoryWriters() { + exists(CdsUtilsModuleAccess utils | + utils.getMember(["mkdirp", "rmdir", "rimraf", "rm"]).getACall() = this + ) + } + + SourceNode directoryWriterUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = directoryWriterUtils(t2).track(t2, t)) + } + + SourceNode directoryWriterUtils() { result = directoryWriterUtils(TypeTracker::end()) } + + DataFlow::Node getPath() { directoryWriterUtils().(DataFlow::CallNode).getAnArgument() = result } +} + +class FileReaders extends DataFlow::Node { + FileReaders() { exists(CdsUtilsModuleAccess utils | utils.getMember(["read"]).getACall() = this) } + + SourceNode fileReaderUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = fileReaderUtils(t2).track(t2, t)) + } + + SourceNode fileReaderUtils() { result = fileReaderUtils(TypeTracker::end()) } + + DataFlow::Node getPath() { fileReaderUtils().(DataFlow::CallNode).getArgument(0) = result } +} + +class FileWriters extends DataFlow::Node { + FileWriters() { + exists(CdsUtilsModuleAccess utils | utils.getMember(["append", "write"]).getACall() = this) + } + + SourceNode fileWriterUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = fileWriterUtils(t2).track(t2, t)) + } + + SourceNode fileWriterUtils() { result = fileWriterUtils(TypeTracker::end()) } + + DataFlow::Node getData() { + exists(DataFlow::CallNode write | + write = fileWriterUtils() and + ( + write.getNumArgument() = 1 and + write.getArgument(0) = result + or + write.getNumArgument() = 2 and + write.getArgument(1) = result + ) + ) + } + + DataFlow::Node getPath() { + exists(DataFlow::CallNode write | + write = fileWriterUtils() and + ( + write.getAMemberCall("to").getAnArgument() = result + or + write.getNumArgument() = 2 and + write.getArgument(0) = result + ) + ) + } +} + +class FileReaderWriters extends DataFlow::Node { + FileReaderWriters() { + exists(CdsUtilsModuleAccess utils | utils.getMember(["copy"]).getACall() = this) + } + + SourceNode fileReaderWriterUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = fileReaderWriterUtils(t2).track(t2, t)) + } + + SourceNode fileReaderWriterUtils() { result = fileReaderWriterUtils(TypeTracker::end()) } + + DataFlow::Node getPath() { + exists(DataFlow::CallNode copy | + copy = fileReaderWriterUtils() and + ( + copy.getAMemberCall("to").getArgument(_) = result + or + copy.getArgument(_) = result + ) + ) + } +} + +abstract class UtilsSink extends DataFlow::Node { } + +abstract class UtilsExtraFlow extends DataFlow::Node { } + +/** + * This represents the data in calls as follows: + * ```javascript + * await write ({foo:'bar'}) .to ('some','file.json') + * ``` + * sinks in this example are: + * ```javascript + * {foo:'bar'} + * ``` + */ +class WrittenData extends UtilsSink { + WrittenData() { exists(FileWriters fw | fw.getData() = this) } +} + +/** + * This represents the filepath in calls as follows: + * ```javascript + * await write ({foo:'bar'}) .to ('some','file.json') + * ``` + * sinks in this example are: + * ```javascript + * 'some' + * 'file.json' + * ``` + */ +class WrittenPath extends UtilsSink { + WrittenPath() { + exists(FileReaders fw | fw.getPath() = this) + or + exists(FileReaderWriters fw | fw.getPath() = this) + or + exists(FileWriters fw | fw.getPath() = this) + or + exists(DirectoryWriters dw | dw.getPath() = this) + or + exists(DirectoryReaders dr | dr.getPath() = this) + } +} + +/** + * This represents calls where the taint flows through the call. e.g. + * ```javascript + * let dir = isdir ('app') + * ``` + */ +class AdditionalFlowStep extends UtilsExtraFlow { + AdditionalFlowStep() { + exists(PathConverters pc | pc.getPath() = this) + or + exists(PathPredicates pr | pr.getPath() = this) + } + + 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/CDS.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDS.qll index 9c263246b..d704a6910 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,81 +1040,3 @@ 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 f08492c2b..25f9f84c2 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 @@ -39,32 +39,3 @@ private SourceNode cdsApplicationServiceInstantiation(TypeTracker t) { SourceNode cdsApplicationServiceInstantiation() { result = cdsApplicationServiceInstantiation(TypeTracker::end()) } - -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 index 586792217..ee8429866 100644 --- a/javascript/frameworks/cap/test/models/cds/utils/utils.expected +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.expected @@ -1,8 +1,8 @@ -| 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:5:21:5:30 | "%E0%A4%A" | "%E0%A4%A": additional flow step | +| utils.js:7:31:7:40 | "%E0%A4%A" | "%E0%A4%A": additional flow step | +| utils.js:9:18:9:27 | "%E0%A4%A" | "%E0%A4%A": additional flow step | +| utils.js:13:17:13:21 | 'app' | 'app': additional flow step | +| utils.js:15:19:15:32 | 'package.json' | 'package.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 | diff --git a/javascript/frameworks/cap/test/models/cds/utils/utils.ql b/javascript/frameworks/cap/test/models/cds/utils/utils.ql index ededca287..14933876f 100644 --- a/javascript/frameworks/cap/test/models/cds/utils/utils.ql +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.ql @@ -1,9 +1,9 @@ import javascript -import advanced_security.javascript.frameworks.cap.CDS +import advanced_security.javascript.frameworks.cap.CAPPathInjectionQuery from DataFlow::Node node, string str, string strfull where - node.(CdsUtils::UtilsSink).toString() = str and strfull = str + ": sink" + node.(UtilsSink).toString() = str and strfull = str + ": sink" or - node.(CdsUtils::UtilsExtraFlow).toString() = str and strfull = str + ": additional flow step" + node.(UtilsExtraFlow).toString() = str and strfull = str + ": additional flow step" select node, strfull From 9d02e7ebedaa0cfdb8b1b5bff90fd88f73c65d39 Mon Sep 17 00:00:00 2001 From: Kristen Newbury Date: Tue, 12 Aug 2025 15:36:57 -0400 Subject: [PATCH 3/6] Partial address comments - refactor qll lib files and add testcase --- .../frameworks/cap/CAPPathInjectionQuery.qll | 164 +----------------- .../javascript/frameworks/cap/CDSUtils.qll | 104 +++++++++++ .../cap/test/models/cds/utils/utils.expected | 2 + .../cap/test/models/cds/utils/utils.js | 16 +- 4 files changed, 122 insertions(+), 164 deletions(-) create mode 100644 javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll index 0cdda7e00..3679a1c22 100644 --- a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll @@ -5,169 +5,7 @@ */ import javascript -import advanced_security.javascript.frameworks.cap.CDS - -/** - * An access to the `utils` module on a CDS facade. - */ -class CdsUtilsModuleAccess extends API::Node { - CdsUtilsModuleAccess() { exists(CdsFacade cds | this = cds.getMember("utils")) } -} - -class PathConverters extends DataFlow::Node { - PathConverters() { - exists(CdsUtilsModuleAccess utils | - utils.getMember(["decodeURI", "decodeURIComponent", "local"]).getACall() = this - ) - } - - SourceNode pathConvertersUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = pathConvertersUtils(t2).track(t2, t)) - } - - SourceNode pathConvertersUtils() { result = pathConvertersUtils(TypeTracker::end()) } - - DataFlow::Node getPath() { pathConvertersUtils().(DataFlow::CallNode).getAnArgument() = result } -} - -class PathPredicates extends DataFlow::Node { - PathPredicates() { - exists(CdsUtilsModuleAccess utils | utils.getMember(["isdir", "isfile"]).getACall() = this) - } - - SourceNode pathPredicateUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = pathPredicateUtils(t2).track(t2, t)) - } - - SourceNode pathPredicateUtils() { result = pathPredicateUtils(TypeTracker::end()) } - - DataFlow::Node getPath() { pathPredicateUtils().(DataFlow::CallNode).getAnArgument() = result } -} - -class DirectoryReaders extends DataFlow::Node { - DirectoryReaders() { - exists(CdsUtilsModuleAccess utils | - utils.getMember(["find", "stat", "readdir"]).getACall() = this - ) - } - - SourceNode directoryReaderUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = directoryReaderUtils(t2).track(t2, t)) - } - - SourceNode directoryReaderUtils() { result = directoryReaderUtils(TypeTracker::end()) } - - DataFlow::Node getPath() { directoryReaderUtils().(DataFlow::CallNode).getAnArgument() = result } -} - -class DirectoryWriters extends DataFlow::Node { - DirectoryWriters() { - exists(CdsUtilsModuleAccess utils | - utils.getMember(["mkdirp", "rmdir", "rimraf", "rm"]).getACall() = this - ) - } - - SourceNode directoryWriterUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = directoryWriterUtils(t2).track(t2, t)) - } - - SourceNode directoryWriterUtils() { result = directoryWriterUtils(TypeTracker::end()) } - - DataFlow::Node getPath() { directoryWriterUtils().(DataFlow::CallNode).getAnArgument() = result } -} - -class FileReaders extends DataFlow::Node { - FileReaders() { exists(CdsUtilsModuleAccess utils | utils.getMember(["read"]).getACall() = this) } - - SourceNode fileReaderUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = fileReaderUtils(t2).track(t2, t)) - } - - SourceNode fileReaderUtils() { result = fileReaderUtils(TypeTracker::end()) } - - DataFlow::Node getPath() { fileReaderUtils().(DataFlow::CallNode).getArgument(0) = result } -} - -class FileWriters extends DataFlow::Node { - FileWriters() { - exists(CdsUtilsModuleAccess utils | utils.getMember(["append", "write"]).getACall() = this) - } - - SourceNode fileWriterUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = fileWriterUtils(t2).track(t2, t)) - } - - SourceNode fileWriterUtils() { result = fileWriterUtils(TypeTracker::end()) } - - DataFlow::Node getData() { - exists(DataFlow::CallNode write | - write = fileWriterUtils() and - ( - write.getNumArgument() = 1 and - write.getArgument(0) = result - or - write.getNumArgument() = 2 and - write.getArgument(1) = result - ) - ) - } - - DataFlow::Node getPath() { - exists(DataFlow::CallNode write | - write = fileWriterUtils() and - ( - write.getAMemberCall("to").getAnArgument() = result - or - write.getNumArgument() = 2 and - write.getArgument(0) = result - ) - ) - } -} - -class FileReaderWriters extends DataFlow::Node { - FileReaderWriters() { - exists(CdsUtilsModuleAccess utils | utils.getMember(["copy"]).getACall() = this) - } - - SourceNode fileReaderWriterUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = fileReaderWriterUtils(t2).track(t2, t)) - } - - SourceNode fileReaderWriterUtils() { result = fileReaderWriterUtils(TypeTracker::end()) } - - DataFlow::Node getPath() { - exists(DataFlow::CallNode copy | - copy = fileReaderWriterUtils() and - ( - copy.getAMemberCall("to").getArgument(_) = result - or - copy.getArgument(_) = result - ) - ) - } -} +import advanced_security.javascript.frameworks.cap.CDSUtils abstract class UtilsSink extends DataFlow::Node { } diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll new file mode 100644 index 000000000..d9933d4cc --- /dev/null +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll @@ -0,0 +1,104 @@ +import javascript +import advanced_security.javascript.frameworks.cap.CDS + +/** + * An access to the `utils` module on a CDS facade. + */ +class CdsUtilsModuleAccess extends API::Node { + CdsUtilsModuleAccess() { exists(CdsFacade cds | this = cds.getMember("utils")) } +} + +class PathConverters extends DataFlow::CallNode { + PathConverters() { + exists(CdsUtilsModuleAccess utils | + utils.getMember(["decodeURI", "decodeURIComponent", "local"]).getACall() = this + ) + } + + DataFlow::Node getPath() { this.getAnArgument() = result } +} + +class PathPredicates extends DataFlow::CallNode { + PathPredicates() { + exists(CdsUtilsModuleAccess utils | utils.getMember(["isdir", "isfile"]).getACall() = this) + } + + DataFlow::Node getPath() { this.getAnArgument() = result } +} + +class DirectoryReaders extends DataFlow::CallNode { + DirectoryReaders() { + exists(CdsUtilsModuleAccess utils | + utils.getMember(["find", "stat", "readdir"]).getACall() = this + ) + } + + DataFlow::Node getPath() { this.getAnArgument() = result } +} + +class DirectoryWriters extends DataFlow::CallNode { + DirectoryWriters() { + exists(CdsUtilsModuleAccess utils | + utils.getMember(["mkdirp", "rmdir", "rimraf", "rm"]).getACall() = this + ) + } + + DataFlow::Node getPath() { this.getAnArgument() = result } +} + +class FileReaders extends DataFlow::CallNode { + FileReaders() { exists(CdsUtilsModuleAccess utils | utils.getMember(["read"]).getACall() = this) } + + DataFlow::Node getPath() { this.getArgument(0) = result } +} + +class FileWriters extends DataFlow::CallNode { + FileWriters() { + exists(CdsUtilsModuleAccess utils | utils.getMember(["append", "write"]).getACall() = this) + } + + SourceNode fileReaderWriterUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = fileReaderWriterUtils(t2).track(t2, t)) + } + + SourceNode fileReaderWriterUtils() { result = fileReaderWriterUtils(TypeTracker::end()) } + + DataFlow::Node getData() { + this.getNumArgument() = 1 and + this.getArgument(0) = result + or + this.getNumArgument() = 2 and + this.getArgument(1) = result + } + + DataFlow::Node getPath() { + fileReaderWriterUtils().getAMemberCall("to").getAnArgument() = result + or + this.getNumArgument() = 2 and + this.getArgument(0) = result + } +} + +class FileReaderWriters extends DataFlow::CallNode { + FileReaderWriters() { + exists(CdsUtilsModuleAccess utils | utils.getMember(["copy"]).getACall() = this) + } + + SourceNode fileReaderWriterUtils(TypeTracker t) { + t.start() and + result = this + or + exists(TypeTracker t2 | result = fileReaderWriterUtils(t2).track(t2, t)) + } + + SourceNode fileReaderWriterUtils() { result = fileReaderWriterUtils(TypeTracker::end()) } + + DataFlow::Node getPath() { + fileReaderWriterUtils().getAMemberCall("to").getArgument(_) = result + or + this.getAnArgument() = result + } +} diff --git a/javascript/frameworks/cap/test/models/cds/utils/utils.expected b/javascript/frameworks/cap/test/models/cds/utils/utils.expected index ee8429866..de55a809a 100644 --- a/javascript/frameworks/cap/test/models/cds/utils/utils.expected +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.expected @@ -35,3 +35,5 @@ | 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 | +| utils.js:52:20:52:28 | 'db/data' | 'db/data': sink | +| utils.js:57:10:57: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 index 9420e4749..3a38f2e5a 100644 --- a/javascript/frameworks/cap/test/models/cds/utils/utils.js +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.js @@ -41,4 +41,18 @@ 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 +await rm('dist/db/data') // sink + +function wrapperouter() { + const temp = append + wrapperinnermid(temp) +} + +function wrapperinnermid(temp) { + const a = temp('db/data') // sink + wrapperinner(a) +} + +function wrapperinner(a) { + a.to('dist/db/data') // sink +} \ No newline at end of file From 9e81393e8eb06fe9795e0088ebf663a05825ce68 Mon Sep 17 00:00:00 2001 From: Kristen Newbury Date: Wed, 13 Aug 2025 13:18:05 -0400 Subject: [PATCH 4/6] Add ql docs CDSUtils --- .../javascript/frameworks/cap/CDSUtils.qll | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll index d9933d4cc..a700434ec 100644 --- a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll @@ -8,6 +8,10 @@ class CdsUtilsModuleAccess extends API::Node { CdsUtilsModuleAccess() { exists(CdsFacade cds | this = cds.getMember("utils")) } } +/** + * CDS Utils: + * `decodeURI`, `decodeURIComponent`, `local` + */ class PathConverters extends DataFlow::CallNode { PathConverters() { exists(CdsUtilsModuleAccess utils | @@ -15,17 +19,31 @@ class PathConverters extends DataFlow::CallNode { ) } + /** + * Gets the arguments to these calls. + */ DataFlow::Node getPath() { this.getAnArgument() = result } } +/** + * CDS Utils: + * `isdir`, `isfile` + */ class PathPredicates extends DataFlow::CallNode { PathPredicates() { exists(CdsUtilsModuleAccess utils | utils.getMember(["isdir", "isfile"]).getACall() = this) } + /** + * Gets the arguments to these calls. + */ DataFlow::Node getPath() { this.getAnArgument() = result } } +/** + * CDS Utils: + * `find`, `stat`, `readdir` + */ class DirectoryReaders extends DataFlow::CallNode { DirectoryReaders() { exists(CdsUtilsModuleAccess utils | @@ -33,9 +51,16 @@ class DirectoryReaders extends DataFlow::CallNode { ) } + /** + * Gets the arguments to these calls. + */ DataFlow::Node getPath() { this.getAnArgument() = result } } +/** + * CDS Utils: + * `mkdirp`, `rmdir`, `rimraf`, `rm` + */ class DirectoryWriters extends DataFlow::CallNode { DirectoryWriters() { exists(CdsUtilsModuleAccess utils | @@ -43,15 +68,29 @@ class DirectoryWriters extends DataFlow::CallNode { ) } + /** + * Gets the arguments to these calls. + */ DataFlow::Node getPath() { this.getAnArgument() = result } } +/** + * CDS Utils: + * `read` + */ class FileReaders extends DataFlow::CallNode { FileReaders() { exists(CdsUtilsModuleAccess utils | utils.getMember(["read"]).getACall() = this) } + /** + * Gets the 0th argument to these calls. + */ DataFlow::Node getPath() { this.getArgument(0) = result } } +/** + * CDS Utils: + * `append`, `write` + */ class FileWriters extends DataFlow::CallNode { FileWriters() { exists(CdsUtilsModuleAccess utils | utils.getMember(["append", "write"]).getACall() = this) @@ -66,6 +105,9 @@ class FileWriters extends DataFlow::CallNode { SourceNode fileReaderWriterUtils() { result = fileReaderWriterUtils(TypeTracker::end()) } + /** + * Gets the arguments to these calls that represent data. + */ DataFlow::Node getData() { this.getNumArgument() = 1 and this.getArgument(0) = result @@ -74,6 +116,10 @@ class FileWriters extends DataFlow::CallNode { this.getArgument(1) = result } + /** + * Gets the arguments to these calls that represent a path. + * Includes arguments to chained calls `to`, where that argument also represents a path. + */ DataFlow::Node getPath() { fileReaderWriterUtils().getAMemberCall("to").getAnArgument() = result or @@ -82,6 +128,10 @@ class FileWriters extends DataFlow::CallNode { } } +/** + * CDS Utils: + * `copy` + */ class FileReaderWriters extends DataFlow::CallNode { FileReaderWriters() { exists(CdsUtilsModuleAccess utils | utils.getMember(["copy"]).getACall() = this) @@ -96,6 +146,10 @@ class FileReaderWriters extends DataFlow::CallNode { SourceNode fileReaderWriterUtils() { result = fileReaderWriterUtils(TypeTracker::end()) } + /** + * Gets the arguments to these calls that represent a path. + * Includes arguments to chained calls `to`, where that argument also represents a path. + */ DataFlow::Node getPath() { fileReaderWriterUtils().getAMemberCall("to").getArgument(_) = result or From 2e409c3b6b11be27fece78b597dd8e12f805c89a Mon Sep 17 00:00:00 2001 From: Kristen Newbury Date: Fri, 15 Aug 2025 11:59:51 -0400 Subject: [PATCH 5/6] Adjust CDSUtil to make simpler rm type tracking that covers rare case --- .../javascript/frameworks/cap/CDSUtils.qll | 22 ++----------------- .../cap/test/models/cds/utils/utils.expected | 1 - .../cap/test/models/cds/utils/utils.js | 2 +- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll index a700434ec..ef8d0c77e 100644 --- a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll @@ -96,15 +96,6 @@ class FileWriters extends DataFlow::CallNode { exists(CdsUtilsModuleAccess utils | utils.getMember(["append", "write"]).getACall() = this) } - SourceNode fileReaderWriterUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = fileReaderWriterUtils(t2).track(t2, t)) - } - - SourceNode fileReaderWriterUtils() { result = fileReaderWriterUtils(TypeTracker::end()) } - /** * Gets the arguments to these calls that represent data. */ @@ -121,7 +112,7 @@ class FileWriters extends DataFlow::CallNode { * Includes arguments to chained calls `to`, where that argument also represents a path. */ DataFlow::Node getPath() { - fileReaderWriterUtils().getAMemberCall("to").getAnArgument() = result + this.getAMemberCall("to").getAnArgument() = result or this.getNumArgument() = 2 and this.getArgument(0) = result @@ -137,21 +128,12 @@ class FileReaderWriters extends DataFlow::CallNode { exists(CdsUtilsModuleAccess utils | utils.getMember(["copy"]).getACall() = this) } - SourceNode fileReaderWriterUtils(TypeTracker t) { - t.start() and - result = this - or - exists(TypeTracker t2 | result = fileReaderWriterUtils(t2).track(t2, t)) - } - - SourceNode fileReaderWriterUtils() { result = fileReaderWriterUtils(TypeTracker::end()) } - /** * Gets the arguments to these calls that represent a path. * Includes arguments to chained calls `to`, where that argument also represents a path. */ DataFlow::Node getPath() { - fileReaderWriterUtils().getAMemberCall("to").getArgument(_) = result + this.getAMemberCall("to").getArgument(_) = result or this.getAnArgument() = result } diff --git a/javascript/frameworks/cap/test/models/cds/utils/utils.expected b/javascript/frameworks/cap/test/models/cds/utils/utils.expected index de55a809a..3ce4caa8a 100644 --- a/javascript/frameworks/cap/test/models/cds/utils/utils.expected +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.expected @@ -36,4 +36,3 @@ | utils.js:43:24:43:29 | 'data' | 'data': sink | | utils.js:44:10:44:23 | 'dist/db/data' | 'dist/db/data': sink | | utils.js:52:20:52:28 | 'db/data' | 'db/data': sink | -| utils.js:57:10:57: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 index 3a38f2e5a..a7e789676 100644 --- a/javascript/frameworks/cap/test/models/cds/utils/utils.js +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.js @@ -54,5 +54,5 @@ function wrapperinnermid(temp) { } function wrapperinner(a) { - a.to('dist/db/data') // sink + a.to('dist/db/data') // sink - [FALSE_NEGATIVE] - rare case as CAP is a fluent API } \ No newline at end of file From fafeffd490a8a981f74ebcecc654671245d7da68 Mon Sep 17 00:00:00 2001 From: Kristen Newbury Date: Tue, 19 Aug 2025 09:44:20 -0400 Subject: [PATCH 6/6] Refactor categories utils --- .../frameworks/cap/CAPPathInjectionQuery.qll | 30 +++++++-- .../javascript/frameworks/cap/CDSUtils.qll | 11 +++- .../cap/test/models/cds/utils/utils.expected | 66 +++++++++---------- .../cap/test/models/cds/utils/utils.ql | 6 +- 4 files changed, 70 insertions(+), 43 deletions(-) diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll index 3679a1c22..352bc94d7 100644 --- a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll @@ -7,7 +7,11 @@ import javascript import advanced_security.javascript.frameworks.cap.CDSUtils -abstract class UtilsSink extends DataFlow::Node { } +abstract class UtilsAccessedPathSink extends DataFlow::Node { } + +abstract class UtilsControlledDataSink extends DataFlow::Node { } + +abstract class UtilsControlledPathSink extends DataFlow::Node { } abstract class UtilsExtraFlow extends DataFlow::Node { } @@ -21,12 +25,26 @@ abstract class UtilsExtraFlow extends DataFlow::Node { } * {foo:'bar'} * ``` */ -class WrittenData extends UtilsSink { +class WrittenData extends UtilsControlledDataSink { WrittenData() { exists(FileWriters fw | fw.getData() = this) } } /** - * This represents the filepath in calls as follows: + * This represents the filepath accessed as an input for the data in calls as follows: + * ```javascript + * await copy('db/data').to('dist/db/data') + * ``` + * sinks in this example are: + * ```javascript + * 'db/data' + * ``` + */ +class AccessedPath extends UtilsAccessedPathSink { + AccessedPath() { exists(FileReaderWriters fw | fw.getFromPath() = this) } +} + +/** + * This represents the filepath where data is written or a file operation is performed in calls as follows: * ```javascript * await write ({foo:'bar'}) .to ('some','file.json') * ``` @@ -36,11 +54,11 @@ class WrittenData extends UtilsSink { * 'file.json' * ``` */ -class WrittenPath extends UtilsSink { - WrittenPath() { +class ControlledInputPath extends UtilsControlledPathSink { + ControlledInputPath() { exists(FileReaders fw | fw.getPath() = this) or - exists(FileReaderWriters fw | fw.getPath() = this) + exists(FileReaderWriters fw | fw.getToPath() = this) or exists(FileWriters fw | fw.getPath() = this) or diff --git a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll index ef8d0c77e..108eb44d1 100644 --- a/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll @@ -129,12 +129,17 @@ class FileReaderWriters extends DataFlow::CallNode { } /** - * Gets the arguments to these calls that represent a path. + * Gets the arguments to these calls that represent a path from which data is read. + */ + DataFlow::Node getFromPath() { this.getArgument(0) = result } + + /** + * Gets the arguments to these calls that represent a path to which data is written. * Includes arguments to chained calls `to`, where that argument also represents a path. */ - DataFlow::Node getPath() { + DataFlow::Node getToPath() { this.getAMemberCall("to").getArgument(_) = result or - this.getAnArgument() = result + this.getArgument(1) = result } } diff --git a/javascript/frameworks/cap/test/models/cds/utils/utils.expected b/javascript/frameworks/cap/test/models/cds/utils/utils.expected index 3ce4caa8a..4ef04b388 100644 --- a/javascript/frameworks/cap/test/models/cds/utils/utils.expected +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.expected @@ -3,36 +3,36 @@ | utils.js:9:18:9:27 | "%E0%A4%A" | "%E0%A4%A": additional flow step | | utils.js:13:17:13:21 | 'app' | 'app': additional flow step | | utils.js:15:19:15:32 | 'package.json' | 'package.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 | -| utils.js:52:20:52:28 | 'db/data' | 'db/data': sink | +| utils.js:17:22:17:35 | 'package.json' | 'package.json': controlled path sink | +| utils.js:19:26:19:39 | 'package.json' | 'package.json': controlled path sink | +| utils.js:21:20:21:33 | 'package.json' | 'package.json': controlled path sink | +| utils.js:23:20:23:33 | 'package.json' | 'package.json': controlled path sink | +| utils.js:25:14:25:22 | 'db/data' | 'db/data': controlled data sink | +| utils.js:25:28:25:41 | 'dist/db/data' | 'dist/db/data': controlled path sink | +| utils.js:26:14:26:22 | 'db/data' | 'db/data': controlled path sink | +| utils.js:26:25:26:38 | 'dist/db/data' | 'dist/db/data': controlled data sink | +| utils.js:28:12:28:20 | 'db/data' | 'db/data': accessed path sink | +| utils.js:28:26:28:39 | 'dist/db/data' | 'dist/db/data': controlled path sink | +| utils.js:29:12:29:20 | 'db/data' | 'db/data': accessed path sink | +| utils.js:29:23:29:36 | 'dist/db/data' | 'dist/db/data': controlled path sink | +| utils.js:31:13:31:26 | { foo: 'bar' } | { foo: 'bar' }: controlled data sink | +| utils.js:31:32:31:47 | 'some/file.json' | 'some/file.json': controlled path sink | +| utils.js:32:13:32:28 | 'some/file.json' | 'some/file.json': controlled path sink | +| utils.js:32:31:32:44 | { foo: 'bar' } | { foo: 'bar' }: controlled data sink | +| utils.js:34:14:34:19 | 'dist' | 'dist': controlled path sink | +| utils.js:34:22:34:25 | 'db' | 'db': controlled path sink | +| utils.js:34:28:34:33 | 'data' | 'data': controlled path sink | +| utils.js:35:14:35:27 | 'dist/db/data' | 'dist/db/data': controlled path sink | +| utils.js:37:13:37:18 | 'dist' | 'dist': controlled path sink | +| utils.js:37:21:37:24 | 'db' | 'db': controlled path sink | +| utils.js:37:27:37:32 | 'data' | 'data': controlled path sink | +| utils.js:38:13:38:26 | 'dist/db/data' | 'dist/db/data': controlled path sink | +| utils.js:40:14:40:19 | 'dist' | 'dist': controlled path sink | +| utils.js:40:22:40:25 | 'db' | 'db': controlled path sink | +| utils.js:40:28:40:33 | 'data' | 'data': controlled path sink | +| utils.js:41:14:41:27 | 'dist/db/data' | 'dist/db/data': controlled path sink | +| utils.js:43:10:43:15 | 'dist' | 'dist': controlled path sink | +| utils.js:43:18:43:21 | 'db' | 'db': controlled path sink | +| utils.js:43:24:43:29 | 'data' | 'data': controlled path sink | +| utils.js:44:10:44:23 | 'dist/db/data' | 'dist/db/data': controlled path sink | +| utils.js:52:20:52:28 | 'db/data' | 'db/data': controlled data sink | diff --git a/javascript/frameworks/cap/test/models/cds/utils/utils.ql b/javascript/frameworks/cap/test/models/cds/utils/utils.ql index 14933876f..14581b14a 100644 --- a/javascript/frameworks/cap/test/models/cds/utils/utils.ql +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.ql @@ -3,7 +3,11 @@ import advanced_security.javascript.frameworks.cap.CAPPathInjectionQuery from DataFlow::Node node, string str, string strfull where - node.(UtilsSink).toString() = str and strfull = str + ": sink" + node.(UtilsControlledPathSink).toString() = str and strfull = str + ": controlled path sink" + or + node.(UtilsAccessedPathSink).toString() = str and strfull = str + ": accessed path sink" + or + node.(UtilsControlledDataSink).toString() = str and strfull = str + ": controlled data sink" or node.(UtilsExtraFlow).toString() = str and strfull = str + ": additional flow step" select node, strfull