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 00000000..3679a1c2 --- /dev/null +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CAPPathInjectionQuery.qll @@ -0,0 +1,69 @@ +/** + * 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.CDSUtils + +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/CDSUtils.qll b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll new file mode 100644 index 00000000..ef8d0c77 --- /dev/null +++ b/javascript/frameworks/cap/lib/advanced_security/javascript/frameworks/cap/CDSUtils.qll @@ -0,0 +1,140 @@ +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")) } +} + +/** + * CDS Utils: + * `decodeURI`, `decodeURIComponent`, `local` + */ +class PathConverters extends DataFlow::CallNode { + PathConverters() { + exists(CdsUtilsModuleAccess utils | + utils.getMember(["decodeURI", "decodeURIComponent", "local"]).getACall() = this + ) + } + + /** + * 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 | + utils.getMember(["find", "stat", "readdir"]).getACall() = this + ) + } + + /** + * 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 | + utils.getMember(["mkdirp", "rmdir", "rimraf", "rm"]).getACall() = this + ) + } + + /** + * 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) + } + + /** + * Gets the arguments to these calls that represent data. + */ + DataFlow::Node getData() { + this.getNumArgument() = 1 and + this.getArgument(0) = result + or + this.getNumArgument() = 2 and + 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() { + this.getAMemberCall("to").getAnArgument() = result + or + this.getNumArgument() = 2 and + this.getArgument(0) = result + } +} + +/** + * CDS Utils: + * `copy` + */ +class FileReaderWriters extends DataFlow::CallNode { + FileReaderWriters() { + exists(CdsUtilsModuleAccess utils | utils.getMember(["copy"]).getACall() = this) + } + + /** + * 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() { + this.getAMemberCall("to").getArgument(_) = result + or + this.getAnArgument() = result + } +} 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..25f9f84c 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,4 @@ private SourceNode cdsApplicationServiceInstantiation(TypeTracker t) { SourceNode cdsApplicationServiceInstantiation() { result = cdsApplicationServiceInstantiation(TypeTracker::end()) -} \ No newline at end of file +} 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..3ce4caa8 --- /dev/null +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.expected @@ -0,0 +1,38 @@ +| 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 | +| 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 | 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..a7e78967 --- /dev/null +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.js @@ -0,0 +1,58 @@ +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 + +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 - [FALSE_NEGATIVE] - rare case as CAP is a fluent API +} \ 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..14933876 --- /dev/null +++ b/javascript/frameworks/cap/test/models/cds/utils/utils.ql @@ -0,0 +1,9 @@ +import javascript +import advanced_security.javascript.frameworks.cap.CAPPathInjectionQuery + +from DataFlow::Node node, string str, string strfull +where + node.(UtilsSink).toString() = str and strfull = str + ": sink" + or + node.(UtilsExtraFlow).toString() = str and strfull = str + ": additional flow step" +select node, strfull