mirror of
https://github.com/github/codeql.git
synced 2026-04-26 09:15:12 +02:00
Added detection for specific Polyfill.io CDN compromise - edited existing library and added new query and tests
This commit is contained in:
@@ -12,158 +12,7 @@
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/** A location that adds a reference to an untrusted source. */
|
||||
abstract class AddsUntrustedUrl extends Locatable {
|
||||
/** Gets an explanation why this source is untrusted. */
|
||||
abstract string getProblem();
|
||||
}
|
||||
|
||||
module StaticCreation {
|
||||
/** Holds if `host` is an alias of localhost. */
|
||||
bindingset[host]
|
||||
predicate isLocalhostPrefix(string host) {
|
||||
host.toLowerCase()
|
||||
.regexpMatch([
|
||||
"(?i)localhost(:[0-9]+)?/.*", "127.0.0.1(:[0-9]+)?/.*", "::1/.*", "\\[::1\\]:[0-9]+/.*"
|
||||
])
|
||||
}
|
||||
|
||||
/** Holds if `url` is a url that is vulnerable to a MITM attack. */
|
||||
bindingset[url]
|
||||
predicate isUntrustedSourceUrl(string url) {
|
||||
exists(string hostPath | hostPath = url.regexpCapture("(?i)http://(.*)", 1) |
|
||||
not isLocalhostPrefix(hostPath)
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if `url` refers to a CDN that needs an integrity check - even with https. */
|
||||
bindingset[url]
|
||||
predicate isCdnUrlWithCheckingRequired(string url) {
|
||||
// Some CDN URLs are required to have an integrity attribute. We only add CDNs to that list
|
||||
// that recommend integrity-checking.
|
||||
url.regexpMatch("(?i)^https?://" +
|
||||
[
|
||||
"code\\.jquery\\.com", //
|
||||
"cdnjs\\.cloudflare\\.com", //
|
||||
"cdnjs\\.com" //
|
||||
] + "/.*\\.js$")
|
||||
}
|
||||
|
||||
/** A script element that refers to untrusted content. */
|
||||
class ScriptElementWithUntrustedContent extends AddsUntrustedUrl instanceof HTML::ScriptElement {
|
||||
ScriptElementWithUntrustedContent() {
|
||||
not exists(string digest | not digest = "" | super.getIntegrityDigest() = digest) and
|
||||
isUntrustedSourceUrl(super.getSourcePath())
|
||||
}
|
||||
|
||||
override string getProblem() { result = "Script loaded using unencrypted connection." }
|
||||
}
|
||||
|
||||
/** A script element that refers to untrusted content. */
|
||||
class CdnScriptElementWithUntrustedContent extends AddsUntrustedUrl, HTML::ScriptElement {
|
||||
CdnScriptElementWithUntrustedContent() {
|
||||
not exists(string digest | not digest = "" | this.getIntegrityDigest() = digest) and
|
||||
isCdnUrlWithCheckingRequired(this.getSourcePath())
|
||||
}
|
||||
|
||||
override string getProblem() {
|
||||
result = "Script loaded from content delivery network with no integrity check."
|
||||
}
|
||||
}
|
||||
|
||||
/** An iframe element that includes untrusted content. */
|
||||
class IframeElementWithUntrustedContent extends AddsUntrustedUrl instanceof HTML::IframeElement {
|
||||
IframeElementWithUntrustedContent() { isUntrustedSourceUrl(super.getSourcePath()) }
|
||||
|
||||
override string getProblem() { result = "Iframe loaded using unencrypted connection." }
|
||||
}
|
||||
}
|
||||
|
||||
module DynamicCreation {
|
||||
/** Holds if `call` creates a tag of kind `name`. */
|
||||
predicate isCreateElementNode(DataFlow::CallNode call, string name) {
|
||||
call = DataFlow::globalVarRef("document").getAMethodCall("createElement") and
|
||||
call.getArgument(0).getStringValue().toLowerCase() = name
|
||||
}
|
||||
|
||||
DataFlow::Node getAttributeAssignmentRhs(DataFlow::CallNode createCall, string name) {
|
||||
result = createCall.getAPropertyWrite(name).getRhs()
|
||||
or
|
||||
exists(DataFlow::InvokeNode inv | inv = createCall.getAMemberInvocation("setAttribute") |
|
||||
inv.getArgument(0).getStringValue() = name and
|
||||
result = inv.getArgument(1)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `createCall` creates a `<script ../>` element which never
|
||||
* has its `integrity` attribute set locally.
|
||||
*/
|
||||
predicate isCreateScriptNodeWoIntegrityCheck(DataFlow::CallNode createCall) {
|
||||
isCreateElementNode(createCall, "script") and
|
||||
not exists(getAttributeAssignmentRhs(createCall, "integrity"))
|
||||
}
|
||||
|
||||
DataFlow::Node urlTrackedFromUnsafeSourceLiteral(DataFlow::TypeTracker t) {
|
||||
t.start() and result.getStringValue().regexpMatch("(?i)http:.*")
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2, DataFlow::Node prev |
|
||||
prev = urlTrackedFromUnsafeSourceLiteral(t2)
|
||||
|
|
||||
not exists(string httpsUrl | httpsUrl.toLowerCase() = "https:" + any(string rest) |
|
||||
// when the result may have a string value starting with https,
|
||||
// we're most likely with an assignment like:
|
||||
// e.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'
|
||||
// these assignments, we don't want to fix - once the browser is using http,
|
||||
// MITM attacks are possible anyway.
|
||||
result.mayHaveStringValue(httpsUrl)
|
||||
) and
|
||||
(
|
||||
t2 = t.smallstep(prev, result)
|
||||
or
|
||||
TaintTracking::sharedTaintStep(prev, result) and
|
||||
t = t2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
DataFlow::Node urlTrackedFromUnsafeSourceLiteral() {
|
||||
result = urlTrackedFromUnsafeSourceLiteral(DataFlow::TypeTracker::end())
|
||||
}
|
||||
|
||||
/** Holds if `sink` is assigned to the attribute `name` of any HTML element. */
|
||||
predicate isAssignedToSrcAttribute(string name, DataFlow::Node sink) {
|
||||
exists(DataFlow::CallNode createElementCall |
|
||||
sink = getAttributeAssignmentRhs(createElementCall, "src") and
|
||||
(
|
||||
name = "script" and
|
||||
isCreateScriptNodeWoIntegrityCheck(createElementCall)
|
||||
or
|
||||
name = "iframe" and
|
||||
isCreateElementNode(createElementCall, "iframe")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class IframeOrScriptSrcAssignment extends AddsUntrustedUrl {
|
||||
string name;
|
||||
|
||||
IframeOrScriptSrcAssignment() {
|
||||
name = ["script", "iframe"] and
|
||||
exists(DataFlow::Node n | n.asExpr() = this |
|
||||
isAssignedToSrcAttribute(name, n) and
|
||||
n = urlTrackedFromUnsafeSourceLiteral()
|
||||
)
|
||||
}
|
||||
|
||||
override string getProblem() {
|
||||
name = "script" and result = "Script loaded using unencrypted connection."
|
||||
or
|
||||
name = "iframe" and result = "Iframe loaded using unencrypted connection."
|
||||
}
|
||||
}
|
||||
}
|
||||
import semmle.javascript.security.FunctionalityFromUntrustedSource
|
||||
|
||||
from AddsUntrustedUrl s
|
||||
select s, s.getProblem()
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Polyfill.io was a popular JavaScript polyflll Content Delivery Network (CDN),
|
||||
used to support new web browser standards on older browsers.
|
||||
<p>
|
||||
|
||||
<p>
|
||||
In February 2024 the domain was sold, and in June 2024 it was widely publicised that the domain
|
||||
had been used to serve malicious scripts. It was taken down later in that month, leaving a window
|
||||
where sites that used the service could have been compromised.
|
||||
<p>
|
||||
|
||||
<p>
|
||||
Including a resource from an untrusted source or using an untrusted channel may
|
||||
allow an attacker to include arbitrary code in the response.
|
||||
When including an external resource (for example, a <code>script</code> element or an
|
||||
<code>iframe</code> element) on a page, it is important to ensure that the received
|
||||
data is not malicious.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Even when <code>https</code> is used, an attacker might still compromise the server.
|
||||
When you use a <code>script</code> element, you should check for subresource integrity -
|
||||
that is, you can check the contents of the data received by supplying a cryptographic
|
||||
digest of the expected sources to the <code>script</code> element. The script will only
|
||||
load sources that match the digest and an attacker will be unable to modify the script
|
||||
even when the server is compromised.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Subresource integrity checking is commonly recommended when importing a fixed version of
|
||||
a library - for example, from a CDN (content-delivery network). Then, the fixed digest
|
||||
of that version of the library can easily be added to the <code>script</code> element's
|
||||
<code>integrity</code> attribute.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
To help mitigate the risk of including a compromised script, consider whether you need to
|
||||
use a polyfill at all, can use a different polyfill CDN service,
|
||||
or could host an uncompromised version of the polyfill yourself.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When using a <code>script</code> element to load a script, it is important to use an
|
||||
<code>https</code> URL and to check subresource integrity.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example loads the Polyfill.io library from the <code>polyfill.io</code> CDN without
|
||||
checking subresource integrity. This use is open to malicious scripts being served by the CDN.
|
||||
</p>
|
||||
|
||||
<sample src="polyfill-nocheck.html" />
|
||||
|
||||
<p>
|
||||
Instead, loading the Polyfill library from a trusted CDN, and checking
|
||||
subresource integrity is recommended, as in the next example.
|
||||
</p>
|
||||
|
||||
<sample src="polyfill-cloudflare-check.html" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Sansec: <a href="https://sansec.io/research/polyfill-supply-chain-attack">Polyfill supply chain attack hits 100K+ sites</a></li>
|
||||
<li>Cloudflare: <a href="https://cdnjs.cloudflare.com/polyfill">Upgrade the web. Automatically. Delivers only the polyfills required by the user's web browser.</a></li>
|
||||
<li>Fastly: <a href="https://community.fastly.com/t/new-options-for-polyfill-io-users/2540">New options for Polyfill.io users</a></li>
|
||||
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Polyfill_(programming)">Polyfill (programming)</a></li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @name Polyfill.io script use
|
||||
* @description Use of script from compromised domain Polyfill.io (https://sansec.io/research/polyfill-supply-chain-attack)
|
||||
* @kind problem
|
||||
* @security-severity 7.2
|
||||
* @problem.severity error
|
||||
* @id js/polyfill-io-compromised-script
|
||||
* @precision high
|
||||
* @tags security
|
||||
* external/cwe/cwe-830
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import semmle.javascript.security.FunctionalityFromUntrustedSource
|
||||
|
||||
from AddsUntrustedUrl s
|
||||
where s.getUrl().regexpMatch("^(?i)https?://(cdn\\.)?polyfill\\.io/.*")
|
||||
select s, "Script loaded from known-compromised content delivery network with no integrity check"
|
||||
9
javascript/ql/src/Security/CWE-830/polyfill-check.html
Normal file
9
javascript/ql/src/Security/CWE-830/polyfill-check.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Polyfill demo - Cloudflare hosted with pinned version and integrity checking</title>
|
||||
<script src="https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=4.8.0" integrity="sha384-3d4jRKquKl90C9aFG+eH4lPJmtbPHgACWHrp+VomFOxF8lzx2jxqeYkhpRg18UWC" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
...
|
||||
</body>
|
||||
</html>
|
||||
9
javascript/ql/src/Security/CWE-830/polyfill-nocheck.html
Normal file
9
javascript/ql/src/Security/CWE-830/polyfill-nocheck.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Polyfill.io demo</title>
|
||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
...
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user