Skip to content

Commit e5d3286

Browse files
authored
Merge pull request github#3183 from asger-semmle/js/bad-url-scheme-check
Approved by esbena
2 parents 0d86866 + 7da0345 commit e5d3286

File tree

4 files changed

+104
-9
lines changed

4 files changed

+104
-9
lines changed

change-notes/1.24/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
| Useless regular-expression character escape (`js/useless-regexp-character-escape`) | Fewer false positive results | This query now distinguishes escapes in strings and regular expression literals. |
8787
| Identical operands (`js/redundant-operation`) | Fewer results | This query now recognizes cases where the operands change a value using ++/-- expressions. |
8888
| Superfluous trailing arguments (`js/superfluous-trailing-arguments`) | Fewer results | This query now recognizes cases where a function uses the `Function.arguments` value to process a variable number of parameters. |
89+
| Incomplete URL scheme check (`js/incomplete-url-scheme-check`) | More results | This query now recognizes more variations of URL scheme checks. |
8990

9091
## Changes to libraries
9192

javascript/ql/src/Security/CWE-020/IncompleteUrlSchemeCheck.ql

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,46 @@
1212
*/
1313

1414
import javascript
15-
import semmle.javascript.dataflow.internal.AccessPaths
1615

1716
/** A URL scheme that can be used to represent executable code. */
1817
class DangerousScheme extends string {
1918
DangerousScheme() { this = "data:" or this = "javascript:" or this = "vbscript:" }
19+
20+
/** Gets the name of this scheme without the `:`. */
21+
string getWithoutColon() { this = result + ":" }
22+
23+
/** Gets the name of this scheme, with or without the `:`. */
24+
string getWithOrWithoutColon() { result = this or result = getWithoutColon() }
25+
}
26+
27+
/** Returns a node that refers to the scheme of `url`. */
28+
DataFlow::SourceNode schemeOf(DataFlow::Node url) {
29+
// url.split(":")[0]
30+
exists(DataFlow::MethodCallNode split |
31+
split.getMethodName() = "split" and
32+
split.getArgument(0).getStringValue() = ":" and
33+
result = split.getAPropertyRead("0") and
34+
url = split.getReceiver()
35+
)
36+
or
37+
// url.getScheme(), url.getProtocol(), getScheme(url), getProtocol(url)
38+
exists(DataFlow::CallNode call |
39+
result = call and
40+
(call.getCalleeName() = "getScheme" or call.getCalleeName() = "getProtocol")
41+
|
42+
call.getNumArgument() = 1 and
43+
url = call.getArgument(0)
44+
or
45+
call.getNumArgument() = 0 and
46+
url = call.getReceiver()
47+
)
48+
or
49+
// url.scheme, url.protocol
50+
exists(DataFlow::PropRead prop |
51+
result = prop and
52+
(prop.getPropertyName() = "scheme" or prop.getPropertyName() = "protocol") and
53+
url = prop.getBase()
54+
)
2055
}
2156

2257
/** Gets a data-flow node that checks `nd` against the given `scheme`. */
@@ -27,6 +62,20 @@ DataFlow::Node schemeCheck(DataFlow::Node nd, DangerousScheme scheme) {
2762
sw.getSubstring().mayHaveStringValue(scheme)
2863
)
2964
or
65+
// check of the form `array.includes(getScheme(nd))`
66+
exists(InclusionTest test, DataFlow::ArrayCreationNode array | test = result |
67+
schemeOf(nd).flowsTo(test.getContainedNode()) and
68+
array.flowsTo(test.getContainerNode()) and
69+
array.getAnElement().mayHaveStringValue(scheme.getWithOrWithoutColon())
70+
)
71+
or
72+
// check of the form `getScheme(nd) === scheme`
73+
exists(EqualityTest test, Expr op1, Expr op2 | test.flow() = result |
74+
test.hasOperands(op1, op2) and
75+
schemeOf(nd).flowsToExpr(op1) and
76+
op2.mayHaveStringValue(scheme.getWithOrWithoutColon())
77+
)
78+
or
3079
// propagate through trimming, case conversion, and regexp replace
3180
exists(DataFlow::MethodCallNode stringop |
3281
stringop.getMethodName().matches("trim%") or
@@ -42,14 +91,14 @@ DataFlow::Node schemeCheck(DataFlow::Node nd, DangerousScheme scheme) {
4291
}
4392

4493
/** Gets a data-flow node that checks an instance of `ap` against the given `scheme`. */
45-
DataFlow::Node schemeCheckOn(AccessPath ap, DangerousScheme scheme) {
46-
result = schemeCheck(ap.getAnInstance().flow(), scheme)
94+
DataFlow::Node schemeCheckOn(DataFlow::SourceNode root, string path, DangerousScheme scheme) {
95+
result = schemeCheck(AccessPath::getAReferenceTo(root, path), scheme)
4796
}
4897

49-
from AccessPath ap, int n
98+
from DataFlow::SourceNode root, string path, int n
5099
where
51100
n = strictcount(DangerousScheme s) and
52-
strictcount(DangerousScheme s | exists(schemeCheckOn(ap, s))) < n
53-
select schemeCheckOn(ap, "javascript:"),
101+
strictcount(DangerousScheme s | exists(schemeCheckOn(root, path, s))) < n
102+
select schemeCheckOn(root, path, "javascript:"),
54103
"This check does not consider " +
55-
strictconcat(DangerousScheme s | not exists(schemeCheckOn(ap, s)) | s, " and ") + "."
104+
strictconcat(DangerousScheme s | not exists(schemeCheckOn(root, path, s)) | s, " and ") + "."
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
| IncompleteUrlSchemeCheck.js:3:9:3:35 | u.start ... ript:") | This check does not consider data: and vbscript:. |
1+
| IncompleteUrlSchemeCheck.js:5:9:5:35 | u.start ... ript:") | This check does not consider data: and vbscript:. |
2+
| IncompleteUrlSchemeCheck.js:16:9:16:39 | badProt ... otocol) | This check does not consider vbscript:. |
3+
| IncompleteUrlSchemeCheck.js:23:9:23:43 | badProt ... scheme) | This check does not consider vbscript:. |
4+
| IncompleteUrlSchemeCheck.js:30:9:30:43 | badProt ... scheme) | This check does not consider vbscript:. |
5+
| IncompleteUrlSchemeCheck.js:37:9:37:31 | scheme ... script" | This check does not consider data: and vbscript:. |
Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
1+
import * as dummy from 'dummy';
2+
13
function sanitizeUrl(url) {
24
let u = decodeURI(url).trim().toLowerCase();
3-
if (u.startsWith("javascript:"))
5+
if (u.startsWith("javascript:")) // NOT OK
6+
return "about:blank";
7+
return url;
8+
}
9+
10+
let badProtocols = ['javascript:', 'data:'];
11+
let badProtocolNoColon = ['javascript', 'data'];
12+
let badProtocolsGood = ['javascript:', 'data:', 'vbscript:'];
13+
14+
function test2(url) {
15+
let protocol = new URL(url).protocol;
16+
if (badProtocols.includes(protocol)) // NOT OK
17+
return "about:blank";
18+
return url;
19+
}
20+
21+
function test3(url) {
22+
let scheme = goog.uri.utils.getScheme(url);
23+
if (badProtocolNoColon.includes(scheme)) // NOT OK
24+
return "about:blank";
25+
return url;
26+
}
27+
28+
function test4(url) {
29+
let scheme = url.split(':')[0];
30+
if (badProtocolNoColon.includes(scheme)) // NOT OK
31+
return "about:blank";
32+
return url;
33+
}
34+
35+
function test5(url) {
36+
let scheme = url.split(':')[0];
37+
if (scheme === "javascript") // NOT OK
38+
return "about:blank";
39+
return url;
40+
}
41+
42+
function test6(url) {
43+
let protocol = new URL(url).protocol;
44+
if (badProtocolsGood.includes(protocol)) // OK
445
return "about:blank";
546
return url;
647
}

0 commit comments

Comments
 (0)