Skip to content

Commit a1b0703

Browse files
committed
Added detection for specific Polyfill.io CDN compromise - edited existing library and added new query and tests
1 parent d9b337c commit a1b0703

11 files changed

+306
-152
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import javascript
2+
3+
/** A location that adds a reference to an untrusted source. */
4+
abstract class AddsUntrustedUrl extends Locatable {
5+
/** Gets an explanation why this source is untrusted. */
6+
abstract string getProblem();
7+
8+
/** Gets the URL of the untrusted source. */
9+
abstract string getUrl();
10+
}
11+
12+
module StaticCreation {
13+
/** Holds if `host` is an alias of localhost. */
14+
bindingset[host]
15+
predicate isLocalhostPrefix(string host) {
16+
host.toLowerCase()
17+
.regexpMatch([
18+
"(?i)localhost(:[0-9]+)?/.*", "127.0.0.1(:[0-9]+)?/.*", "::1/.*", "\\[::1\\]:[0-9]+/.*"
19+
])
20+
}
21+
22+
/** Holds if `url` is a url that is vulnerable to a MITM attack. */
23+
bindingset[url]
24+
predicate isUntrustedSourceUrl(string url) {
25+
exists(string hostPath | hostPath = url.regexpCapture("(?i)http://(.*)", 1) |
26+
not isLocalhostPrefix(hostPath)
27+
)
28+
}
29+
30+
/** Holds if `url` refers to a CDN that needs an integrity check - even with https. */
31+
bindingset[url]
32+
predicate isCdnUrlWithCheckingRequired(string url) {
33+
// Some CDN URLs are required to have an integrity attribute. We only add CDNs to that list
34+
// that recommend integrity-checking.
35+
url.regexpMatch("(?i)^https?://" +
36+
[
37+
"code\\.jquery\\.com", //
38+
"cdnjs\\.cloudflare\\.com", //
39+
"cdnjs\\.com", //
40+
"cdn\\.polyfill\\.io", // compromised
41+
"polyfill\\.io", // compromised
42+
] + "/.*\\.js$")
43+
}
44+
45+
/** A script element that refers to untrusted content. */
46+
class ScriptElementWithUntrustedContent extends AddsUntrustedUrl instanceof HTML::ScriptElement {
47+
ScriptElementWithUntrustedContent() {
48+
not exists(string digest | not digest = "" | super.getIntegrityDigest() = digest) and
49+
isUntrustedSourceUrl(super.getSourcePath())
50+
}
51+
52+
override string getUrl() { result = super.getSourcePath() }
53+
54+
override string getProblem() { result = "Script loaded using unencrypted connection." }
55+
}
56+
57+
/** A script element that refers to untrusted content. */
58+
class CdnScriptElementWithUntrustedContent extends AddsUntrustedUrl, HTML::ScriptElement {
59+
CdnScriptElementWithUntrustedContent() {
60+
not exists(string digest | not digest = "" | this.getIntegrityDigest() = digest) and
61+
isCdnUrlWithCheckingRequired(this.getSourcePath())
62+
}
63+
64+
override string getUrl() { result = this.getSourcePath() }
65+
66+
override string getProblem() {
67+
result = "Script loaded from content delivery network with no integrity check."
68+
}
69+
}
70+
71+
/** An iframe element that includes untrusted content. */
72+
class IframeElementWithUntrustedContent extends AddsUntrustedUrl instanceof HTML::IframeElement {
73+
IframeElementWithUntrustedContent() { isUntrustedSourceUrl(super.getSourcePath()) }
74+
75+
override string getUrl() { result = super.getSourcePath() }
76+
77+
override string getProblem() { result = "Iframe loaded using unencrypted connection." }
78+
}
79+
}
80+
81+
module DynamicCreation {
82+
/** Holds if `call` creates a tag of kind `name`. */
83+
predicate isCreateElementNode(DataFlow::CallNode call, string name) {
84+
call = DataFlow::globalVarRef("document").getAMethodCall("createElement") and
85+
call.getArgument(0).getStringValue().toLowerCase() = name
86+
}
87+
88+
DataFlow::Node getAttributeAssignmentRhs(DataFlow::CallNode createCall, string name) {
89+
result = createCall.getAPropertyWrite(name).getRhs()
90+
or
91+
exists(DataFlow::InvokeNode inv | inv = createCall.getAMemberInvocation("setAttribute") |
92+
inv.getArgument(0).getStringValue() = name and
93+
result = inv.getArgument(1)
94+
)
95+
}
96+
97+
/**
98+
* Holds if `createCall` creates a `<script ../>` element which never
99+
* has its `integrity` attribute set locally.
100+
*/
101+
predicate isCreateScriptNodeWoIntegrityCheck(DataFlow::CallNode createCall) {
102+
isCreateElementNode(createCall, "script") and
103+
not exists(getAttributeAssignmentRhs(createCall, "integrity"))
104+
}
105+
106+
DataFlow::Node urlTrackedFromUnsafeSourceLiteral(DataFlow::TypeTracker t) {
107+
t.start() and result.getStringValue().regexpMatch("(?i)http:.*")
108+
or
109+
exists(DataFlow::TypeTracker t2, DataFlow::Node prev |
110+
prev = urlTrackedFromUnsafeSourceLiteral(t2)
111+
|
112+
not exists(string httpsUrl | httpsUrl.toLowerCase() = "https:" + any(string rest) |
113+
// when the result may have a string value starting with https,
114+
// we're most likely with an assignment like:
115+
// e.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'
116+
// these assignments, we don't want to fix - once the browser is using http,
117+
// MITM attacks are possible anyway.
118+
result.mayHaveStringValue(httpsUrl)
119+
) and
120+
(
121+
t2 = t.smallstep(prev, result)
122+
or
123+
TaintTracking::sharedTaintStep(prev, result) and
124+
t = t2
125+
)
126+
)
127+
}
128+
129+
DataFlow::Node urlTrackedFromUnsafeSourceLiteral() {
130+
result = urlTrackedFromUnsafeSourceLiteral(DataFlow::TypeTracker::end())
131+
}
132+
133+
/** Holds if `sink` is assigned to the attribute `name` of any HTML element. */
134+
predicate isAssignedToSrcAttribute(string name, DataFlow::Node sink) {
135+
exists(DataFlow::CallNode createElementCall |
136+
sink = getAttributeAssignmentRhs(createElementCall, "src") and
137+
(
138+
name = "script" and
139+
isCreateScriptNodeWoIntegrityCheck(createElementCall)
140+
or
141+
name = "iframe" and
142+
isCreateElementNode(createElementCall, "iframe")
143+
)
144+
)
145+
}
146+
147+
class IframeOrScriptSrcAssignment extends AddsUntrustedUrl {
148+
string name;
149+
150+
IframeOrScriptSrcAssignment() {
151+
name = ["script", "iframe"] and
152+
exists(DataFlow::Node n | n.asExpr() = this |
153+
isAssignedToSrcAttribute(name, n) and
154+
n = urlTrackedFromUnsafeSourceLiteral()
155+
)
156+
}
157+
158+
override string getUrl() {
159+
exists(DataFlow::Node n | n.asExpr() = this |
160+
isAssignedToSrcAttribute(name, n) and
161+
result = n.getStringValue()
162+
)
163+
}
164+
165+
override string getProblem() {
166+
name = "script" and result = "Script loaded using unencrypted connection."
167+
or
168+
name = "iframe" and result = "Iframe loaded using unencrypted connection."
169+
}
170+
}
171+
}

javascript/ql/src/Security/CWE-830/FunctionalityFromUntrustedSource.ql

Lines changed: 1 addition & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -12,158 +12,7 @@
1212
*/
1313

1414
import javascript
15-
16-
/** A location that adds a reference to an untrusted source. */
17-
abstract class AddsUntrustedUrl extends Locatable {
18-
/** Gets an explanation why this source is untrusted. */
19-
abstract string getProblem();
20-
}
21-
22-
module StaticCreation {
23-
/** Holds if `host` is an alias of localhost. */
24-
bindingset[host]
25-
predicate isLocalhostPrefix(string host) {
26-
host.toLowerCase()
27-
.regexpMatch([
28-
"(?i)localhost(:[0-9]+)?/.*", "127.0.0.1(:[0-9]+)?/.*", "::1/.*", "\\[::1\\]:[0-9]+/.*"
29-
])
30-
}
31-
32-
/** Holds if `url` is a url that is vulnerable to a MITM attack. */
33-
bindingset[url]
34-
predicate isUntrustedSourceUrl(string url) {
35-
exists(string hostPath | hostPath = url.regexpCapture("(?i)http://(.*)", 1) |
36-
not isLocalhostPrefix(hostPath)
37-
)
38-
}
39-
40-
/** Holds if `url` refers to a CDN that needs an integrity check - even with https. */
41-
bindingset[url]
42-
predicate isCdnUrlWithCheckingRequired(string url) {
43-
// Some CDN URLs are required to have an integrity attribute. We only add CDNs to that list
44-
// that recommend integrity-checking.
45-
url.regexpMatch("(?i)^https?://" +
46-
[
47-
"code\\.jquery\\.com", //
48-
"cdnjs\\.cloudflare\\.com", //
49-
"cdnjs\\.com" //
50-
] + "/.*\\.js$")
51-
}
52-
53-
/** A script element that refers to untrusted content. */
54-
class ScriptElementWithUntrustedContent extends AddsUntrustedUrl instanceof HTML::ScriptElement {
55-
ScriptElementWithUntrustedContent() {
56-
not exists(string digest | not digest = "" | super.getIntegrityDigest() = digest) and
57-
isUntrustedSourceUrl(super.getSourcePath())
58-
}
59-
60-
override string getProblem() { result = "Script loaded using unencrypted connection." }
61-
}
62-
63-
/** A script element that refers to untrusted content. */
64-
class CdnScriptElementWithUntrustedContent extends AddsUntrustedUrl, HTML::ScriptElement {
65-
CdnScriptElementWithUntrustedContent() {
66-
not exists(string digest | not digest = "" | this.getIntegrityDigest() = digest) and
67-
isCdnUrlWithCheckingRequired(this.getSourcePath())
68-
}
69-
70-
override string getProblem() {
71-
result = "Script loaded from content delivery network with no integrity check."
72-
}
73-
}
74-
75-
/** An iframe element that includes untrusted content. */
76-
class IframeElementWithUntrustedContent extends AddsUntrustedUrl instanceof HTML::IframeElement {
77-
IframeElementWithUntrustedContent() { isUntrustedSourceUrl(super.getSourcePath()) }
78-
79-
override string getProblem() { result = "Iframe loaded using unencrypted connection." }
80-
}
81-
}
82-
83-
module DynamicCreation {
84-
/** Holds if `call` creates a tag of kind `name`. */
85-
predicate isCreateElementNode(DataFlow::CallNode call, string name) {
86-
call = DataFlow::globalVarRef("document").getAMethodCall("createElement") and
87-
call.getArgument(0).getStringValue().toLowerCase() = name
88-
}
89-
90-
DataFlow::Node getAttributeAssignmentRhs(DataFlow::CallNode createCall, string name) {
91-
result = createCall.getAPropertyWrite(name).getRhs()
92-
or
93-
exists(DataFlow::InvokeNode inv | inv = createCall.getAMemberInvocation("setAttribute") |
94-
inv.getArgument(0).getStringValue() = name and
95-
result = inv.getArgument(1)
96-
)
97-
}
98-
99-
/**
100-
* Holds if `createCall` creates a `<script ../>` element which never
101-
* has its `integrity` attribute set locally.
102-
*/
103-
predicate isCreateScriptNodeWoIntegrityCheck(DataFlow::CallNode createCall) {
104-
isCreateElementNode(createCall, "script") and
105-
not exists(getAttributeAssignmentRhs(createCall, "integrity"))
106-
}
107-
108-
DataFlow::Node urlTrackedFromUnsafeSourceLiteral(DataFlow::TypeTracker t) {
109-
t.start() and result.getStringValue().regexpMatch("(?i)http:.*")
110-
or
111-
exists(DataFlow::TypeTracker t2, DataFlow::Node prev |
112-
prev = urlTrackedFromUnsafeSourceLiteral(t2)
113-
|
114-
not exists(string httpsUrl | httpsUrl.toLowerCase() = "https:" + any(string rest) |
115-
// when the result may have a string value starting with https,
116-
// we're most likely with an assignment like:
117-
// e.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'
118-
// these assignments, we don't want to fix - once the browser is using http,
119-
// MITM attacks are possible anyway.
120-
result.mayHaveStringValue(httpsUrl)
121-
) and
122-
(
123-
t2 = t.smallstep(prev, result)
124-
or
125-
TaintTracking::sharedTaintStep(prev, result) and
126-
t = t2
127-
)
128-
)
129-
}
130-
131-
DataFlow::Node urlTrackedFromUnsafeSourceLiteral() {
132-
result = urlTrackedFromUnsafeSourceLiteral(DataFlow::TypeTracker::end())
133-
}
134-
135-
/** Holds if `sink` is assigned to the attribute `name` of any HTML element. */
136-
predicate isAssignedToSrcAttribute(string name, DataFlow::Node sink) {
137-
exists(DataFlow::CallNode createElementCall |
138-
sink = getAttributeAssignmentRhs(createElementCall, "src") and
139-
(
140-
name = "script" and
141-
isCreateScriptNodeWoIntegrityCheck(createElementCall)
142-
or
143-
name = "iframe" and
144-
isCreateElementNode(createElementCall, "iframe")
145-
)
146-
)
147-
}
148-
149-
class IframeOrScriptSrcAssignment extends AddsUntrustedUrl {
150-
string name;
151-
152-
IframeOrScriptSrcAssignment() {
153-
name = ["script", "iframe"] and
154-
exists(DataFlow::Node n | n.asExpr() = this |
155-
isAssignedToSrcAttribute(name, n) and
156-
n = urlTrackedFromUnsafeSourceLiteral()
157-
)
158-
}
159-
160-
override string getProblem() {
161-
name = "script" and result = "Script loaded using unencrypted connection."
162-
or
163-
name = "iframe" and result = "Iframe loaded using unencrypted connection."
164-
}
165-
}
166-
}
15+
import semmle.javascript.security.FunctionalityFromUntrustedSource
16716

16817
from AddsUntrustedUrl s
16918
select s, s.getProblem()

0 commit comments

Comments
 (0)