Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added detection for untrusted domains in script content - edited existing library and added new query and tests ("the Polyfill PR") #16886

Merged
merged 22 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a1b0703
Added detection for specific Polyfill.io CDN compromise - edited exis…
aegilops Jul 1, 2024
ceda46e
Fixed ending <p> tags
aegilops Jul 1, 2024
1744a98
Added full stop to end of message
aegilops Jul 1, 2024
c985c9a
Added change note for polyfill.io query
aegilops Jul 1, 2024
b4d8c48
Fixed wrong name for example HTML
aegilops Jul 1, 2024
73fc6bc
Added some missing QLDoc
aegilops Jul 1, 2024
d289fb4
Merge branch 'main' into aegilops/polyfill-io-compromised-script
aegilops Jul 1, 2024
e2b37f9
Added dot to end of test message
aegilops Jul 1, 2024
1fe14e2
Split out "compromised" functionality
aegilops Jul 8, 2024
86afd54
Moved new query to 'experimental'
aegilops Jul 9, 2024
dae2aeb
QLDoc
aegilops Jul 9, 2024
0aab2ae
Formatting of QLL
aegilops Jul 9, 2024
01ec7c2
Fixed test
aegilops Jul 9, 2024
d71be8a
Moved from `experimental` into default queries
aegilops Jul 11, 2024
3f37fe6
Apply suggestions from code review - docs and wording
aegilops Jul 12, 2024
040f948
Added a note that SRI can be considered for some dynamic services
aegilops Jul 12, 2024
00d91dc
Created guide on customizing these queries, and referenced it in the …
aegilops Jul 12, 2024
61df4d2
Merge branch 'aegilops/polyfill-io-compromised-script' of https://git…
aegilops Jul 12, 2024
c9af53f
Merge branch 'main' into aegilops/polyfill-io-compromised-script
aegilops Jul 12, 2024
11249e7
Apply suggestions from code review - docs tweaks of CUSTOMIZING.md
aegilops Jul 12, 2024
79980a9
Added links to eventual location of CUSTOMIZING.md
aegilops Jul 12, 2024
de5ec1f
Merge branch 'main' into aegilops/polyfill-io-compromised-script
aegilops Jul 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import javascript

Check warning on line 1 in javascript/ql/lib/semmle/javascript/security/FunctionalityFromUntrustedSource.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for file FunctionalityFromUntrustedSource

/** 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();

/** Gets the URL of the untrusted source. */
abstract string getUrl();
}

module StaticCreation {

Check warning on line 12 in javascript/ql/lib/semmle/javascript/security/FunctionalityFromUntrustedSource.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for module FunctionalityFromUntrustedSource::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", //
"cdn\\.polyfill\\.io", // compromised
"polyfill\\.io", // compromised
] + "/.*\\.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 getUrl() { result = 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 getUrl() { result = 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 getUrl() { result = super.getSourcePath() }

override string getProblem() { result = "Iframe loaded using unencrypted connection." }
}
}

module DynamicCreation {

Check warning on line 81 in javascript/ql/lib/semmle/javascript/security/FunctionalityFromUntrustedSource.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for module FunctionalityFromUntrustedSource::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) {

Check warning on line 88 in javascript/ql/lib/semmle/javascript/security/FunctionalityFromUntrustedSource.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for classless-predicate FunctionalityFromUntrustedSource::DynamicCreation::getAttributeAssignmentRhs/2
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) {

Check warning on line 106 in javascript/ql/lib/semmle/javascript/security/FunctionalityFromUntrustedSource.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for classless-predicate FunctionalityFromUntrustedSource::DynamicCreation::urlTrackedFromUnsafeSourceLiteral/1
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() {

Check warning on line 129 in javascript/ql/lib/semmle/javascript/security/FunctionalityFromUntrustedSource.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for classless-predicate FunctionalityFromUntrustedSource::DynamicCreation::urlTrackedFromUnsafeSourceLiteral/0
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 {

Check warning on line 147 in javascript/ql/lib/semmle/javascript/security/FunctionalityFromUntrustedSource.qll

View workflow job for this annotation

GitHub Actions / qldoc

Missing QLdoc for class FunctionalityFromUntrustedSource::DynamicCreation::IframeOrScriptSrcAssignment
string name;

IframeOrScriptSrcAssignment() {
name = ["script", "iframe"] and
exists(DataFlow::Node n | n.asExpr() = this |
isAssignedToSrcAttribute(name, n) and
n = urlTrackedFromUnsafeSourceLiteral()
)
}

override string getUrl() {
exists(DataFlow::Node n | n.asExpr() = this |
isAssignedToSrcAttribute(name, n) and
result = n.getStringValue()
)
}

override string getProblem() {
name = "script" and result = "Script loaded using unencrypted connection."
or
name = "iframe" and result = "Iframe loaded using unencrypted connection."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading
Loading