/** * Provides classes for finding functionality that is loaded from untrusted sources and used in script or frame elements. */ 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(); /** Gets the URL of the untrusted source. */ abstract string getUrl(); } /** Looks for static creation of an element and source. */ 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. exists(string hostname, string requiredCheckingHostname | hostname = url.regexpCapture("(?i)^(?:https?:)?//([^/]+)/.*\\.js$", 1) and isCdnDomainWithCheckingRequired(requiredCheckingHostname) and hostname = requiredCheckingHostname ) } /** 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()) or isUrlWithUntrustedDomain(super.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." } } } /** Holds if `url` refers to an URL that uses an untrusted domain. */ bindingset[url] predicate isUrlWithUntrustedDomain(string url) { exists(string hostname | hostname = url.regexpCapture("(?i)^(?:https?:)?//([^/]+)/.*", 1) and isUntrustedHostname(hostname) ) } /** Holds if `hostname` refers to a domain or subdomain that is untrusted. */ bindingset[hostname] predicate isUntrustedHostname(string hostname) { exists(string domain | (hostname = domain or hostname.matches("%." + domain)) and isUntrustedDomain(domain) ) } // The following predicates are extended in data extensions under javascript/ql/lib/semmle/javascript/security/domains/ // and can be extended with custom model packs as necessary. /** Holds for hostnames defined in data extensions */ extensible predicate isCdnDomainWithCheckingRequired(string hostname); /** Holds for domains defined in data extensions */ extensible predicate isUntrustedDomain(string domain); /** Looks for dyanmic creation of an element and source. */ 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 } /** Get the right-hand side of an assignment to a named attribute. */ 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 `