|
| 1 | +/** |
| 2 | + * Provides classes for finding functionality that is loaded from untrusted sources and used in script or frame elements. |
| 3 | + */ |
| 4 | + |
| 5 | +import javascript |
| 6 | + |
| 7 | +/** A location that adds a reference to an untrusted source. */ |
| 8 | +abstract class AddsUntrustedUrl extends Locatable { |
| 9 | + /** Gets an explanation why this source is untrusted. */ |
| 10 | + abstract string getProblem(); |
| 11 | + |
| 12 | + /** Gets the URL of the untrusted source. */ |
| 13 | + abstract string getUrl(); |
| 14 | +} |
| 15 | + |
| 16 | +/** Looks for static creation of an element and source. */ |
| 17 | +module StaticCreation { |
| 18 | + /** Holds if `host` is an alias of localhost. */ |
| 19 | + bindingset[host] |
| 20 | + predicate isLocalhostPrefix(string host) { |
| 21 | + host.toLowerCase() |
| 22 | + .regexpMatch([ |
| 23 | + "(?i)localhost(:[0-9]+)?/.*", "127.0.0.1(:[0-9]+)?/.*", "::1/.*", "\\[::1\\]:[0-9]+/.*" |
| 24 | + ]) |
| 25 | + } |
| 26 | + |
| 27 | + /** Holds if `url` is a url that is vulnerable to a MITM attack. */ |
| 28 | + bindingset[url] |
| 29 | + predicate isUntrustedSourceUrl(string url) { |
| 30 | + exists(string hostPath | hostPath = url.regexpCapture("(?i)http://(.*)", 1) | |
| 31 | + not isLocalhostPrefix(hostPath) |
| 32 | + ) |
| 33 | + } |
| 34 | + |
| 35 | + /** Holds if `url` refers to a CDN that needs an integrity check - even with https. */ |
| 36 | + bindingset[url] |
| 37 | + predicate isCdnUrlWithCheckingRequired(string url) { |
| 38 | + // Some CDN URLs are required to have an integrity attribute. We only add CDNs to that list |
| 39 | + // that recommend integrity-checking. |
| 40 | + exists(string hostname, string requiredCheckingHostname | |
| 41 | + hostname = url.regexpCapture("(?i)^(?:https?:)?//([^/]+)/.*\\.js$", 1) and |
| 42 | + isCdnDomainWithCheckingRequired(requiredCheckingHostname) and |
| 43 | + hostname = requiredCheckingHostname |
| 44 | + ) |
| 45 | + } |
| 46 | + |
| 47 | + /** A script element that refers to untrusted content. */ |
| 48 | + class ScriptElementWithUntrustedContent extends AddsUntrustedUrl instanceof HTML::ScriptElement { |
| 49 | + ScriptElementWithUntrustedContent() { |
| 50 | + not exists(string digest | not digest = "" | super.getIntegrityDigest() = digest) and |
| 51 | + isUntrustedSourceUrl(super.getSourcePath()) |
| 52 | + } |
| 53 | + |
| 54 | + override string getUrl() { result = super.getSourcePath() } |
| 55 | + |
| 56 | + override string getProblem() { result = "Script loaded using unencrypted connection." } |
| 57 | + } |
| 58 | + |
| 59 | + /** A script element that refers to untrusted content. */ |
| 60 | + class CdnScriptElementWithUntrustedContent extends AddsUntrustedUrl, HTML::ScriptElement { |
| 61 | + CdnScriptElementWithUntrustedContent() { |
| 62 | + not exists(string digest | not digest = "" | this.getIntegrityDigest() = digest) and |
| 63 | + ( |
| 64 | + isCdnUrlWithCheckingRequired(this.getSourcePath()) |
| 65 | + or |
| 66 | + isUrlWithUntrustedDomain(super.getSourcePath()) |
| 67 | + ) |
| 68 | + } |
| 69 | + |
| 70 | + override string getUrl() { result = this.getSourcePath() } |
| 71 | + |
| 72 | + override string getProblem() { |
| 73 | + result = "Script loaded from content delivery network with no integrity check." |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + /** An iframe element that includes untrusted content. */ |
| 78 | + class IframeElementWithUntrustedContent extends AddsUntrustedUrl instanceof HTML::IframeElement { |
| 79 | + IframeElementWithUntrustedContent() { isUntrustedSourceUrl(super.getSourcePath()) } |
| 80 | + |
| 81 | + override string getUrl() { result = super.getSourcePath() } |
| 82 | + |
| 83 | + override string getProblem() { result = "Iframe loaded using unencrypted connection." } |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +/** Holds if `url` refers to an URL that uses an untrusted domain. */ |
| 88 | +bindingset[url] |
| 89 | +predicate isUrlWithUntrustedDomain(string url) { |
| 90 | + exists(string hostname | |
| 91 | + hostname = url.regexpCapture("(?i)^(?:https?:)?//([^/]+)/.*", 1) and |
| 92 | + isUntrustedHostname(hostname) |
| 93 | + ) |
| 94 | +} |
| 95 | + |
| 96 | +/** Holds if `hostname` refers to a domain or subdomain that is untrusted. */ |
| 97 | +bindingset[hostname] |
| 98 | +predicate isUntrustedHostname(string hostname) { |
| 99 | + exists(string domain | |
| 100 | + (hostname = domain or hostname.matches("%." + domain)) and |
| 101 | + isUntrustedDomain(domain) |
| 102 | + ) |
| 103 | +} |
| 104 | + |
| 105 | +// The following predicates are extended in data extensions under javascript/ql/lib/semmle/javascript/security/domains/ |
| 106 | +// and can be extended with custom model packs as necessary. |
| 107 | +/** Holds for hostnames defined in data extensions */ |
| 108 | +extensible predicate isCdnDomainWithCheckingRequired(string hostname); |
| 109 | + |
| 110 | +/** Holds for domains defined in data extensions */ |
| 111 | +extensible predicate isUntrustedDomain(string domain); |
| 112 | + |
| 113 | +/** Looks for dyanmic creation of an element and source. */ |
| 114 | +module DynamicCreation { |
| 115 | + /** Holds if `call` creates a tag of kind `name`. */ |
| 116 | + predicate isCreateElementNode(DataFlow::CallNode call, string name) { |
| 117 | + call = DataFlow::globalVarRef("document").getAMethodCall("createElement") and |
| 118 | + call.getArgument(0).getStringValue().toLowerCase() = name |
| 119 | + } |
| 120 | + |
| 121 | + /** Get the right-hand side of an assignment to a named attribute. */ |
| 122 | + DataFlow::Node getAttributeAssignmentRhs(DataFlow::CallNode createCall, string name) { |
| 123 | + result = createCall.getAPropertyWrite(name).getRhs() |
| 124 | + or |
| 125 | + exists(DataFlow::InvokeNode inv | inv = createCall.getAMemberInvocation("setAttribute") | |
| 126 | + inv.getArgument(0).getStringValue() = name and |
| 127 | + result = inv.getArgument(1) |
| 128 | + ) |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Holds if `createCall` creates a `<script ../>` element which never |
| 133 | + * has its `integrity` attribute set locally. |
| 134 | + */ |
| 135 | + predicate isCreateScriptNodeWoIntegrityCheck(DataFlow::CallNode createCall) { |
| 136 | + isCreateElementNode(createCall, "script") and |
| 137 | + not exists(getAttributeAssignmentRhs(createCall, "integrity")) |
| 138 | + } |
| 139 | + |
| 140 | + /** Holds if `t` tracks a URL that is loaded from an untrusted source. */ |
| 141 | + DataFlow::Node urlTrackedFromUnsafeSourceLiteral(DataFlow::TypeTracker t) { |
| 142 | + t.start() and result.getStringValue().regexpMatch("(?i)http:.*") |
| 143 | + or |
| 144 | + exists(DataFlow::TypeTracker t2, DataFlow::Node prev | |
| 145 | + prev = urlTrackedFromUnsafeSourceLiteral(t2) |
| 146 | + | |
| 147 | + not exists(string httpsUrl | httpsUrl.toLowerCase() = "https:" + any(string rest) | |
| 148 | + // when the result may have a string value starting with https, |
| 149 | + // we're most likely with an assignment like: |
| 150 | + // e.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js' |
| 151 | + // these assignments, we don't want to fix - once the browser is using http, |
| 152 | + // MITM attacks are possible anyway. |
| 153 | + result.mayHaveStringValue(httpsUrl) |
| 154 | + ) and |
| 155 | + ( |
| 156 | + t2 = t.smallstep(prev, result) |
| 157 | + or |
| 158 | + TaintTracking::sharedTaintStep(prev, result) and |
| 159 | + t = t2 |
| 160 | + ) |
| 161 | + ) |
| 162 | + } |
| 163 | + |
| 164 | + /** Holds a dataflow node is traked from an untrusted source. */ |
| 165 | + DataFlow::Node urlTrackedFromUnsafeSourceLiteral() { |
| 166 | + result = urlTrackedFromUnsafeSourceLiteral(DataFlow::TypeTracker::end()) |
| 167 | + } |
| 168 | + |
| 169 | + /** Holds if `sink` is assigned to the attribute `name` of any HTML element. */ |
| 170 | + predicate isAssignedToSrcAttribute(string name, DataFlow::Node sink) { |
| 171 | + exists(DataFlow::CallNode createElementCall | |
| 172 | + sink = getAttributeAssignmentRhs(createElementCall, "src") and |
| 173 | + ( |
| 174 | + name = "script" and |
| 175 | + isCreateScriptNodeWoIntegrityCheck(createElementCall) |
| 176 | + or |
| 177 | + name = "iframe" and |
| 178 | + isCreateElementNode(createElementCall, "iframe") |
| 179 | + ) |
| 180 | + ) |
| 181 | + } |
| 182 | + |
| 183 | + /** A script or iframe element that refers to untrusted content. */ |
| 184 | + class IframeOrScriptSrcAssignment extends AddsUntrustedUrl { |
| 185 | + string name; |
| 186 | + |
| 187 | + IframeOrScriptSrcAssignment() { |
| 188 | + name = ["script", "iframe"] and |
| 189 | + exists(DataFlow::Node n | n.asExpr() = this | |
| 190 | + isAssignedToSrcAttribute(name, n) and |
| 191 | + n = urlTrackedFromUnsafeSourceLiteral() |
| 192 | + ) |
| 193 | + } |
| 194 | + |
| 195 | + override string getUrl() { |
| 196 | + exists(DataFlow::Node n | n.asExpr() = this | |
| 197 | + isAssignedToSrcAttribute(name, n) and |
| 198 | + result = n.getStringValue() |
| 199 | + ) |
| 200 | + } |
| 201 | + |
| 202 | + override string getProblem() { |
| 203 | + name = "script" and result = "Script loaded using unencrypted connection." |
| 204 | + or |
| 205 | + name = "iframe" and result = "Iframe loaded using unencrypted connection." |
| 206 | + } |
| 207 | + } |
| 208 | +} |
0 commit comments