diff --git a/javascript/ql/lib/change-notes/2025-03-14-escape.md b/javascript/ql/lib/change-notes/2025-03-14-escape.md new file mode 100644 index 000000000000..334fd6cc04d4 --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-03-14-escape.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added additional flow step for `unescape()` and `escape()`. diff --git a/javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll b/javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll index 2dd1f622fb5b..862cf1b84274 100644 --- a/javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll +++ b/javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll @@ -494,7 +494,8 @@ module TaintTracking { succ = c and c = DataFlow::globalVarRef([ - "encodeURI", "decodeURI", "encodeURIComponent", "decodeURIComponent", "unescape" + "encodeURI", "decodeURI", "encodeURIComponent", "decodeURIComponent", "unescape", + "escape" ]).getACall() and pred = c.getArgument(0) ) diff --git a/javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll b/javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll index dc23b895a4f6..f863b86a3b57 100644 --- a/javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll +++ b/javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll @@ -892,7 +892,13 @@ module TaintedPath { TaintTracking::uriStep(node1, node2) or exists(DataFlow::CallNode decode | - decode.getCalleeName() = "decodeURIComponent" or decode.getCalleeName() = "decodeURI" + decode = + DataFlow::globalVarRef([ + "decodeURIComponent", + "decodeURI", + "escape", + "unescape" + ]).getACall() | node1 = decode.getArgument(0) and node2 = decode diff --git a/javascript/ql/lib/semmle/javascript/security/dataflow/Xss.qll b/javascript/ql/lib/semmle/javascript/security/dataflow/Xss.qll index 0d17c9d84943..41487b8c3b64 100644 --- a/javascript/ql/lib/semmle/javascript/security/dataflow/Xss.qll +++ b/javascript/ql/lib/semmle/javascript/security/dataflow/Xss.qll @@ -53,7 +53,7 @@ module Shared { class UriEncodingSanitizer extends Sanitizer, DataFlow::CallNode { UriEncodingSanitizer() { exists(string name | this = DataFlow::globalVarRef(name).getACall() | - name = "encodeURI" or name = "encodeURIComponent" + name in ["encodeURI", "encodeURIComponent", "escape"] ) } } diff --git a/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.expected b/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.expected index 2c19dc96c898..17c3e12dedda 100644 --- a/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.expected +++ b/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.expected @@ -45,6 +45,8 @@ | TaintedPath.js:195:29:195:85 | path.re ... '), '') | TaintedPath.js:191:24:191:30 | req.url | TaintedPath.js:195:29:195:85 | path.re ... '), '') | This path depends on a $@. | TaintedPath.js:191:24:191:30 | req.url | user-provided value | | TaintedPath.js:202:29:202:68 | path.re ... '), '') | TaintedPath.js:200:24:200:30 | req.url | TaintedPath.js:202:29:202:68 | path.re ... '), '') | This path depends on a $@. | TaintedPath.js:200:24:200:30 | req.url | user-provided value | | TaintedPath.js:205:31:205:69 | path.re ... '), '') | TaintedPath.js:200:24:200:30 | req.url | TaintedPath.js:205:31:205:69 | path.re ... '), '') | This path depends on a $@. | TaintedPath.js:200:24:200:30 | req.url | user-provided value | +| TaintedPath.js:214:29:214:42 | improperEscape | TaintedPath.js:212:24:212:30 | req.url | TaintedPath.js:214:29:214:42 | improperEscape | This path depends on a $@. | TaintedPath.js:212:24:212:30 | req.url | user-provided value | +| TaintedPath.js:216:29:216:43 | improperEscape2 | TaintedPath.js:212:24:212:30 | req.url | TaintedPath.js:216:29:216:43 | improperEscape2 | This path depends on a $@. | TaintedPath.js:212:24:212:30 | req.url | user-provided value | | examples/TaintedPath.js:10:29:10:43 | ROOT + filePath | examples/TaintedPath.js:8:28:8:34 | req.url | examples/TaintedPath.js:10:29:10:43 | ROOT + filePath | This path depends on a $@. | examples/TaintedPath.js:8:28:8:34 | req.url | user-provided value | | express.js:8:20:8:32 | req.query.bar | express.js:8:20:8:32 | req.query.bar | express.js:8:20:8:32 | req.query.bar | This path depends on a $@. | express.js:8:20:8:32 | req.query.bar | user-provided value | | handlebars.js:11:32:11:39 | filePath | handlebars.js:29:46:29:60 | req.params.path | handlebars.js:11:32:11:39 | filePath | This path depends on a $@. | handlebars.js:29:46:29:60 | req.params.path | user-provided value | @@ -320,6 +322,18 @@ edges | TaintedPath.js:200:24:200:30 | req.url | TaintedPath.js:200:14:200:37 | url.par ... , true) | provenance | Config | | TaintedPath.js:202:29:202:32 | path | TaintedPath.js:202:29:202:68 | path.re ... '), '') | provenance | Config | | TaintedPath.js:205:31:205:34 | path | TaintedPath.js:205:31:205:69 | path.re ... '), '') | provenance | Config | +| TaintedPath.js:212:7:212:48 | path | TaintedPath.js:213:33:213:36 | path | provenance | | +| TaintedPath.js:212:7:212:48 | path | TaintedPath.js:215:36:215:39 | path | provenance | | +| TaintedPath.js:212:14:212:37 | url.par ... , true) | TaintedPath.js:212:14:212:43 | url.par ... ).query | provenance | Config | +| TaintedPath.js:212:14:212:43 | url.par ... ).query | TaintedPath.js:212:14:212:48 | url.par ... ry.path | provenance | Config | +| TaintedPath.js:212:14:212:48 | url.par ... ry.path | TaintedPath.js:212:7:212:48 | path | provenance | | +| TaintedPath.js:212:24:212:30 | req.url | TaintedPath.js:212:14:212:37 | url.par ... , true) | provenance | Config | +| TaintedPath.js:213:9:213:37 | improperEscape | TaintedPath.js:214:29:214:42 | improperEscape | provenance | | +| TaintedPath.js:213:26:213:37 | escape(path) | TaintedPath.js:213:9:213:37 | improperEscape | provenance | | +| TaintedPath.js:213:33:213:36 | path | TaintedPath.js:213:26:213:37 | escape(path) | provenance | Config | +| TaintedPath.js:215:9:215:40 | improperEscape2 | TaintedPath.js:216:29:216:43 | improperEscape2 | provenance | | +| TaintedPath.js:215:27:215:40 | unescape(path) | TaintedPath.js:215:9:215:40 | improperEscape2 | provenance | | +| TaintedPath.js:215:36:215:39 | path | TaintedPath.js:215:27:215:40 | unescape(path) | provenance | Config | | examples/TaintedPath.js:8:7:8:52 | filePath | examples/TaintedPath.js:10:36:10:43 | filePath | provenance | | | examples/TaintedPath.js:8:18:8:41 | url.par ... , true) | examples/TaintedPath.js:8:18:8:47 | url.par ... ).query | provenance | Config | | examples/TaintedPath.js:8:18:8:47 | url.par ... ).query | examples/TaintedPath.js:8:18:8:52 | url.par ... ry.path | provenance | Config | @@ -780,6 +794,19 @@ nodes | TaintedPath.js:202:29:202:68 | path.re ... '), '') | semmle.label | path.re ... '), '') | | TaintedPath.js:205:31:205:34 | path | semmle.label | path | | TaintedPath.js:205:31:205:69 | path.re ... '), '') | semmle.label | path.re ... '), '') | +| TaintedPath.js:212:7:212:48 | path | semmle.label | path | +| TaintedPath.js:212:14:212:37 | url.par ... , true) | semmle.label | url.par ... , true) | +| TaintedPath.js:212:14:212:43 | url.par ... ).query | semmle.label | url.par ... ).query | +| TaintedPath.js:212:14:212:48 | url.par ... ry.path | semmle.label | url.par ... ry.path | +| TaintedPath.js:212:24:212:30 | req.url | semmle.label | req.url | +| TaintedPath.js:213:9:213:37 | improperEscape | semmle.label | improperEscape | +| TaintedPath.js:213:26:213:37 | escape(path) | semmle.label | escape(path) | +| TaintedPath.js:213:33:213:36 | path | semmle.label | path | +| TaintedPath.js:214:29:214:42 | improperEscape | semmle.label | improperEscape | +| TaintedPath.js:215:9:215:40 | improperEscape2 | semmle.label | improperEscape2 | +| TaintedPath.js:215:27:215:40 | unescape(path) | semmle.label | unescape(path) | +| TaintedPath.js:215:36:215:39 | path | semmle.label | path | +| TaintedPath.js:216:29:216:43 | improperEscape2 | semmle.label | improperEscape2 | | examples/TaintedPath.js:8:7:8:52 | filePath | semmle.label | filePath | | examples/TaintedPath.js:8:18:8:41 | url.par ... , true) | semmle.label | url.par ... , true) | | examples/TaintedPath.js:8:18:8:47 | url.par ... ).query | semmle.label | url.par ... ).query | diff --git a/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.js b/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.js index ba57e930f03f..7e9d09b0d4af 100644 --- a/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.js +++ b/javascript/ql/test/query-tests/Security/CWE-022/TaintedPath/TaintedPath.js @@ -208,3 +208,10 @@ var server = http.createServer(function(req, res) { } }); +var srv = http.createServer(function(req, res) { + let path = url.parse(req.url, true).query.path; // $ Source + const improperEscape = escape(path); + res.write(fs.readFileSync(improperEscape)); // $ Alert + const improperEscape2 = unescape(path); + res.write(fs.readFileSync(improperEscape2)); // $ Alert +}); diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/string-manipulations.js b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/string-manipulations.js index d20f88dba398..895970f2b735 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/string-manipulations.js +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/string-manipulations.js @@ -8,5 +8,5 @@ document.write(document.location.href.toUpperCase()); // $ Alert document.write(document.location.href.trimLeft()); // $ Alert document.write(String.fromCharCode(document.location.href)); // $ Alert document.write(String(document.location.href)); // $ Alert -document.write(escape(document.location.href)); // OK - for now -document.write(escape(escape(escape(document.location.href)))); // OK - for now +document.write(escape(document.location.href)); +document.write(escape(escape(escape(document.location.href))));