diff --git a/javascript/lib/browserextension/BothSidesRequestForgeryQuery.qll b/javascript/lib/browserextension/BothSidesRequestForgeryQuery.qll new file mode 100644 index 00000000..b0685e22 --- /dev/null +++ b/javascript/lib/browserextension/BothSidesRequestForgeryQuery.qll @@ -0,0 +1,68 @@ +/** + * Provides a taint-tracking configuration for reasoning about client-side + * request forgery. + * + * Note, for performance reasons: only import this file if + * the `Configuration` class is needed, otherwise + * `RequestForgeryCustomizations` should be imported instead. + */ + + import javascript + import semmle.javascript.security.dataflow.UrlConcatenation + import semmle.javascript.security.dataflow.RequestForgeryCustomizations::RequestForgery + import BrowserAPI + + /** + * A taint tracking configuration for client-side request forgery. + * Server side is disabled since this is in the browser, but the extra models can be enabled for extra coverage + */ + class Configuration extends TaintTracking::Configuration { + Configuration() { this = "ClientSideRequestForgery" } + + override predicate isSource(DataFlow::Node source) { + exists(Source src | + source = src and + not src.isServerSide() + ) or + source instanceof OnMessageExternal or source instanceof OnConnectExternal + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof Sink } + + override predicate isSanitizer(DataFlow::Node node) { + super.isSanitizer(node) or + node instanceof Sanitizer + } + + override predicate isSanitizerOut(DataFlow::Node node) { sanitizingPrefixEdge(node, _) } + + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { + isAdditionalRequestForgeryStep(pred, succ) + } + } + + class BrowserStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + (exists (DataFlow::ParameterNode p | + pred instanceof SendMessage and + succ = p and + p.getParameter() instanceof AddListener + )) + } + } + + class ReturnStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + (exists (DataFlow::ParameterNode p | + succ instanceof SendMessageReturnValue and + pred = p.getAnInvocation().getArgument(0) and + p.getParameter() instanceof AddListenerReturn + )) + } + } + + class AwaitStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ){ + succ.asExpr() instanceof AwaitExpr and pred.asExpr() = succ.asExpr().(AwaitExpr).getOperand() + } + } \ No newline at end of file diff --git a/javascript/lib/browserextension/BrowserAPI.qll b/javascript/lib/browserextension/BrowserAPI.qll new file mode 100644 index 00000000..d6f6c089 --- /dev/null +++ b/javascript/lib/browserextension/BrowserAPI.qll @@ -0,0 +1,600 @@ +import javascript + +/** + * Provides classes modeling dangerous sinks for browser extension APIs. + * Currently supports browser.download, browser.contentsettings, browser.tabs and chrome.runtime + */ + +module Browser { + /** + * A data flow node that should be considered a source of the `browser/chrome` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = [DataFlow::globalVarRef("browser"), DataFlow::globalVarRef("chrome")] } + } +} + +/** + * Gets a direct reference to the `browser` object. + */ +DataFlow::SourceNode browserSource() { result instanceof Browser::Range } + +/** + * Gets a reference to the `browser` object. + */ +private DataFlow::SourceNode browserRef(DataFlow::TypeTracker t) { + t.start() and + result instanceof Browser::Range + or + exists(DataFlow::TypeTracker t2 | result = browserRef(t2).track(t2, t)) +} + +/** + * Gets a reference to the 'browser' object. + */ +DataFlow::SourceNode browserRef() { result = browserRef(DataFlow::TypeTracker::end()) } + +module TabsSource { + /** + * A data flow node that should be considered a source of the browser `tabs` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("tabs") } + } +} + +/** Gets a reference to a browser `tabs` object. */ +private DataFlow::SourceNode tabsRef(DataFlow::TypeTracker t) { + t.start() and + result = tabsSource() + or + t.startInProp("tabs") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = tabsRef(t2).track(t2, t)) +} + +/** Gets a reference to a browser `tabs` object. */ +DataFlow::SourceNode tabsRef() { result = tabsRef(DataFlow::TypeTracker::end()) } + +/** + * Gets a direct reference to the browser `tabs` object. + */ +DataFlow::SourceNode tabsSource() { result instanceof TabsSource::Range } + + +module WindowsSource { + /** + * A data flow node that should be considered a source of the browser `windows` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("windows") } + } +} + +/** Gets a reference to a browser `tabs` object. */ +private DataFlow::SourceNode windowsRef(DataFlow::TypeTracker t) { + t.start() and + result = windowsSource() + or + t.startInProp("windows") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = windowsRef(t2).track(t2, t)) +} + +/** Gets a reference to a browser `tabs` object. */ +DataFlow::SourceNode windowsRef() { result = windowsRef(DataFlow::TypeTracker::end()) } + +/** + * Gets a direct reference to the browser `tabs` object. + */ +DataFlow::SourceNode windowsSource() { result instanceof WindowsSource::Range } + +module CookiesSource { + /** + * A data flow node that should be considered a source of the browser `cookies` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("cookies") } + } + } + + /** Gets a reference to a browser `cookies` object. */ + private DataFlow::SourceNode cookiesRef(DataFlow::TypeTracker t) { + t.start() and + result = cookiesSource() + or + t.startInProp("cookies") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = cookiesRef(t2).track(t2, t)) + } + + /** + * Gets a reference to a browser `cookies` object. + */ + DataFlow::SourceNode cookiesRef() { result = cookiesRef(DataFlow::TypeTracker::end()) } + + /** + * Gets a direct reference to the browser `cookies` object. + */ + DataFlow::SourceNode cookiesSource() { result instanceof CookiesSource::Range } + + + + module BrowsingDataSource { + /** + * A data flow node that should be considered a source of the browser `browsingData` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("browsingData") } + } + } + + /** Gets a reference to a browser `browsingData` object. */ + private DataFlow::SourceNode browsingDataRef(DataFlow::TypeTracker t) { + t.start() and + result = browsingDataSource() + or + t.startInProp("browsingData") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = browsingDataRef(t2).track(t2, t)) + } + + /** + * Gets a reference to a browser `browsingData` object. + */ + DataFlow::SourceNode browsingDataRef() { result = browsingDataRef(DataFlow::TypeTracker::end()) } + + /** + * Gets a direct reference to the browser `browsingData` object. + */ + DataFlow::SourceNode browsingDataSource() { result instanceof BrowsingDataSource::Range } + + + + + + + module BookmarksSource { + /** + * A data flow node that should be considered a source of the browser `bookmarks` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("bookmarks") } + } + } + + /** Gets a reference to a browser `bookmarks` object. */ + private DataFlow::SourceNode bookmarksRef(DataFlow::TypeTracker t) { + t.start() and + result = bookmarksSource() + or + t.startInProp("bookmarks") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = bookmarksRef(t2).track(t2, t)) + } + + /** + * Gets a reference to a browser `bookmarks` object. + */ + DataFlow::SourceNode bookmarksRef() { result = bookmarksRef(DataFlow::TypeTracker::end()) } + + /** + * Gets a direct reference to the browser `bookmarks` object. + */ + DataFlow::SourceNode bookmarksSource() { result instanceof BookmarksSource::Range } + + + + + + + module HistorySource { + /** + * A data flow node that should be considered a source of the browser `history` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("history") } + } + } + + /** Gets a reference to a browser `history` object. */ + private DataFlow::SourceNode historyRef(DataFlow::TypeTracker t) { + t.start() and + result = historySource() + or + t.startInProp("history") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = historyRef(t2).track(t2, t)) + } + + /** + * Gets a reference to a browser `history` object. + */ + DataFlow::SourceNode historyRef() { result = historyRef(DataFlow::TypeTracker::end()) } + + /** + * Gets a direct reference to the browser `history` object. + */ + DataFlow::SourceNode historySource() { result instanceof HistorySource::Range } + + + + module StorageSource { + /** + * A data flow node that should be considered a source of the browser `storage` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("storage") } + } + } + + /** Gets a reference to a browser `storage` object. */ + private DataFlow::SourceNode storageRef(DataFlow::TypeTracker t) { + t.start() and + result = storageSource() + or + t.startInProp("storage") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = storageRef(t2).track(t2, t)) + } + + /** + * Gets a reference to a browser `storage` object. + */ + DataFlow::SourceNode storageRef() { result = storageRef(DataFlow::TypeTracker::end()) } + + /** + * Gets a direct reference to the browser `storage` object. + */ + DataFlow::SourceNode storageSource() { result instanceof StorageSource::Range } + + + + + + + module StorageTypeSource { + /** + * A data flow node that should be considered a source of the browser.storage `type` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = storageRef().getAPropertyRead(["local", "managed", "session"]) } + } + } + + /** Gets a reference to a browser `storage` object. */ + private DataFlow::SourceNode storagetypeRef(DataFlow::TypeTracker t) { + t.start() and + result = storagetypeSource() + or + t.startInProp(["local", "managed", "session"]) and + result = [storageSource()] + or + exists(DataFlow::TypeTracker t2 | result = storagetypeRef(t2).track(t2, t)) + } + + /** + * Gets a reference to a browser.storage `type` object. + */ + DataFlow::SourceNode storagetypeRef() { result = storagetypeRef(DataFlow::TypeTracker::end()) } + + /** + * Gets a direct reference to the browser.storage `type` object. + */ + DataFlow::SourceNode storagetypeSource() { result instanceof StorageTypeSource::Range } + + module TopSitesSource { + /** + * A data flow node that should be considered a source of the browser `topSites` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("topSites") } + } + } + + /** Gets a reference to a browser `topSites` object. */ + private DataFlow::SourceNode topSitesRef(DataFlow::TypeTracker t) { + t.start() and + result = topSitesSource() + or + t.startInProp("topSites") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = topSitesRef(t2).track(t2, t)) + } + + /** + * Gets a reference to a browser `topSites` object. + */ + DataFlow::SourceNode topSitesRef() { result = topSitesRef(DataFlow::TypeTracker::end()) } + + /** + * Gets a direct reference to the browser `topSites` object. + */ + DataFlow::SourceNode topSitesSource() { result instanceof TopSitesSource::Range } + +/** + * Sink for chrome.tabs.executeScript() which may allow an allow arbitrary javascript execution. + */ +class ExecuteScript extends DataFlow::Node { + ExecuteScript() { exists( DataFlow::CallNode c | + c = tabsRef().getAMethodCall("executeScript") | (this = c.getArgument(0) and c.getNumArgument() = 1) + or + (this = c.getArgument(1) and c.getNumArgument() = 2 ) )} +} + +module DownloadSource { + /** + * A data flow node that should be considered a source of the chrome `download` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { this = browserRef().getAPropertyRead("downloads") } + } +} + +private DataFlow::SourceNode downloadsRef(DataFlow::TypeTracker t) { + t.start() and + result = downloadsSource() + or + t.startInProp("downloads") and + result = [browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = downloadsRef(t2).track(t2, t)) +} + + /** + * Gets a reference to a browser `downloads` object. + */ +DataFlow::SourceNode downloadsRef() { result = downloadsRef(DataFlow::TypeTracker::end()) } + + /** + * Gets a direct reference to the browser `downloads` object. + */ +DataFlow::SourceNode downloadsSource() { result instanceof TabsSource::Range } + + + +module ContentSettingsSource { + /** + * A data flow node that should be considered a source of the DOM `contentsettings` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { + exists(string propName | this = browserRef().getAPropertyRead(propName) | + propName = ["contentSettings"] + ) + } + } +} + +private DataFlow::SourceNode contentSettingsRef(DataFlow::TypeTracker t) { + t.start() and + result = contentSettingsSource() + or + t.startInProp("contentSettings") and + result = [DataFlow::globalObjectRef(), browserSource()] + or + exists(DataFlow::TypeTracker t2 | result = contentSettingsRef(t2).track(t2, t)) +} + +/** + * Gets a reference to a `contentSettings` object. + */ + +DataFlow::SourceNode contentSettingsRef() { result = contentSettingsRef(DataFlow::TypeTracker::end()) } + +/** + * Gets a direct reference to the `contentSettings` object. + */ + +DataFlow::SourceNode contentSettingsSource() { result instanceof ContentSettingsSource::Range } + +/** + * A specific content setting for chrome extensions. + * Ex: chrome.contentsettings.cookies + */ +module ContentSettingsSettingsSource { + /** + * A data flow node that should be considered a source of the chrome.contentsettings `property` object. + * + * Can be subclassed to add additional such nodes. + */ + abstract class Range extends DataFlow::Node { } + + class DefaultRange extends Range { + DefaultRange() { + exists(string propName | this = contentSettingsRef().getAPropertyRead(propName) | + propName = + [ + "cookies", "images", "javascript", "location", "popups", "notifications", "microphone", + "camera", "automaticDownloads" + ] + ) + } + } +} + +/** + * A specific content setting for chrome extensions. + * Ex: chrome.contentsettings.cookies + */ +private DataFlow::SourceNode contentSettingsSettingsRef(DataFlow::TypeTracker t) { + t.start() and + result = contentSettingsSettingsSource() + or + t.startInProp([ + "cookies", "images", "javascript", "location", "popups", "notifications", "microphone", + "camera", "automaticDownloads" + ]) and + result = [contentSettingsSource()] + or + exists(DataFlow::TypeTracker t2 | result = contentSettingsSettingsRef(t2).track(t2, t)) +} + +/** + * Gets a reference to a `chrome.contentSetting.property` object. + */ +DataFlow::SourceNode contentSettingsSettingsRef() { + result = contentSettingsSettingsRef(DataFlow::TypeTracker::end()) +} + +/** + * Gets a direct reference to the `chrome.contentSetting.property` object. + */ +DataFlow::SourceNode contentSettingsSettingsSource() { + result instanceof ContentSettingsSettingsSource::Range +} + + +/** + * chrome.runtime dataflow node + */ +class Runtime extends DataFlow::SourceNode { + Runtime() { this = browserRef().getAPropertyRead("runtime").(DataFlow::SourceNode) } +} + +/** + * chrome.runtime.onMessage.AddListner() + */ +class AddListener extends Parameter { + AddListener() { + exists(Runtime r | + r.getAPropertyRead("onMessage").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(0) = this + ) + } +} + +/** + * chrome.runtime.onMessage.AddListner() + */ +class AddListenerReturn extends Parameter { + AddListenerReturn() { + exists(Runtime r | + r.getAPropertyRead("onMessage").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(2) = this + ) + } +} + +/** + * chrome.runtime.sendMessage() + */ +class SendMessage extends DataFlow::Node { + SendMessage() { exists(Runtime r | (r.getAMethodCall("sendMessage").getArgument(1) = this and r.getAMethodCall("sendMessage").getNumArgument() = 3) + or (r.getAMethodCall("sendMessage").getArgument(0) = this and r.getAMethodCall("sendMessage").getNumArgument() < 3 ))} +} + +/** + * chrome.runtime.sendMessage() return value + */ +class SendMessageReturnValue extends DataFlow::SourceNode { + SendMessageReturnValue() { (browserRef().getAPropertyRead("runtime").(DataFlow::SourceNode).getAMethodCall("sendMessage") = this)} +} + + + +/** + * Source + * chrome.runtime.onConnectExternal.addListener + */ + +class OnConnectExternal extends DataFlow::Node { + OnConnectExternal() { exists(Runtime r, DataFlow::ParameterNode p| r.getAPropertyRead("onConnectExternal").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(0) = p.getParameter() and p = this)} +} + +/** + * Source + * chrome.runtime.onConnectExternal.addListener + */ + + class OnConnectExternalFunction extends DataFlow::Node { + OnConnectExternalFunction() { exists(Runtime r | r.getAPropertyRead("onConnectExternal").(DataFlow::SourceNode).getAMethodCall("addListener") = this)} +} + +/** + * Source + * chrome.runtime.onMessageExternal.addListener + */ + + class OnMessageExternal extends DataFlow::Node { + OnMessageExternal() { exists(Runtime r, DataFlow::ParameterNode p | r.getAPropertyRead("onMessageExternal").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(0) = p.getParameter() and this = p)} +} + + /** + * Source + * chrome.runtime.onMessageExternal.addListener sender parameter + */ + + class OnMessageExternalSender extends DataFlow::ParameterNode { + OnMessageExternalSender() { exists(Runtime r, DataFlow::ParameterNode pp | r.getAPropertyRead("onMessageExternal").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(1) = pp.getParameter() and this = pp)} +} + + + +//Return value is important, but how to model output? +/** + * Sink + * chrome.topSites.get() + */ +class GetTopSites extends DataFlow::Node { + GetTopSites() {topSitesRef().getAMethodCall("get") = this} +} + + + + + diff --git a/javascript/lib/browserextension/BrowserInjectionFieldCustomizations.qll b/javascript/lib/browserextension/BrowserInjectionFieldCustomizations.qll new file mode 100644 index 00000000..3ccab77d --- /dev/null +++ b/javascript/lib/browserextension/BrowserInjectionFieldCustomizations.qll @@ -0,0 +1,200 @@ +/** + * Provides default sources, sinks and sanitizers for reasoning about + * Chrome API injection vulnerabilities, as well as extension points for + * adding your own. + */ +import javascript +private import browserextension.BrowserAPI +private import semmle.javascript.security.dataflow.XssThroughDomCustomizations::XssThroughDom as XssThroughDom + +module BrowserInjection { + + private import DataFlow::FlowLabel + /** + * A data flow source for Chrome API injection vulnerabilities. + */ + abstract class Source extends DataFlow::Node { + + + + DataFlow::FlowLabel getFlowLabel() { result = "BrowserSource" } + } + + /** + * A data flow sink for Chrome API injection vulnerabilities. + */ + abstract class Sink extends DataFlow::Node { + } + + + +/** + * Sink for chrome.tabs.update() which may allow an allow an arbitrary redirect if + * user input is used. + */ +class Update extends Sink { + Update() {exists (DataFlow::CallNode c | c = tabsRef().getAMethodCall("update") and this = c.getArgument(c.getNumArgument()-1))} + } + +/* + * A sink for chrome extensions that may allow an attacker to download a file, or make an arbitrary request + */ + + class DownloadsDangerous extends Sink { + DownloadsDangerous() { this = downloadsRef().getAMethodCall("download").getArgument(0) } +} + +/* + * A sink for chrome extensions that may allow an attacker to remove a file. + */ + +class DownloadsRemoveFile extends Sink { + DownloadsRemoveFile() { this = downloadsRef().getAMethodCall("removeFile").getArgument(0) } +} + + + +/** + * Requires reading the return value + * Sink + * chrome.cookies.getAll() + */ +class GetCookie extends Sink { + GetCookie() {cookiesRef().getAMethodCall(["get","getAll"]).getArgument(0) = this} +} + +/** + * + * Sink + * chrome.history.search() + */ +class AddHistory extends Sink { + AddHistory() {historyRef().getAMethodCall("addUrl").getArgument(0) = this} +} + +/** + * Requires reading the return value + * Sink + * chrome.history.search() + */ +class SearchHistory extends Sink { + SearchHistory() {historyRef().getAMethodCall("search").getArgument(0) = this} +} + +/** + * Sink + * chrome.history.deleteUrl/deleteRange + */ +class Delete extends Sink { + Delete() {historyRef().getAMethodCall(["deleteUrl","deleteRange"]).getArgument(0) = this} +} + +/** + * chrome.bookmarks.remove/update() + */ +class UpdateBookmarks extends DataFlow::Node { + UpdateBookmarks() {bookmarksRef().getAMethodCall(["remove", "update"]).getArgument([0,1]) = this} +} + +/** + * chrome.browsingData.removePasswords() + */ +class RemoveBrowsingData extends Sink { + RemoveBrowsingData() {this = browsingDataRef().getAMethodCall("removePasswords").getArgument(0)} + +} + +/** + * chrome.windows.create() + */ +class CreateWindows extends Sink { + CreateWindows() {this = windowsRef().getAMethodCall("create").getArgument(0)} + +} + +/** + * chrome.windows.remove() + */ +class RemoveWindows extends Sink { + RemoveWindows() {this = windowsRef().getAMethodCall("create").getArgument(0)} + +} + + + + + //Firefox only + //browser.management.removePasswords() +// class ManagementEnable extends Sink { +// ManagementEnable() {managementRef().getAMethodCall("setEnabled").getArgument([0,1])} + +// } + +/** + * SINK WITH NO ARGUMENTS + * Requires reading the return value + * chrome.bookmarks.getTree() this has no arguments + */ + + class GetTreeBookmarks extends DataFlow::Node { + GetTreeBookmarks() {bookmarksRef().getAMethodCall("getTree") = this} +} + + + + +/** + * Source + * chrome.runtime.onConnectExternal.addListener + */ + + class OnConnectExternalProxy extends Source instanceof OnConnectExternal { + OnConnectExternalProxy() { exists(Runtime r, DataFlow::ParameterNode p| r.getAPropertyRead("onConnectExternal").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(0) = p.getParameter() and p = this)} +} + +/** +* Source +* chrome.runtime.onMessageExternal.addListener +*/ + +class OnMessageExternalProxy extends Source instanceof OnMessageExternal { + OnMessageExternalProxy() { exists(Runtime r, DataFlow::ParameterNode p | r.getAPropertyRead("onMessageExternal").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(0) = p.getParameter() and this = p)} +} + +/** +* Source +* noisy, use as needed +*/ + +//class XSSDOMProxy extends Source instanceof XssThroughDom::Source{} + +class RemoteFlowSourceProxy extends Source instanceof RemoteFlowSource{} + + +class BrowserStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + (exists (DataFlow::ParameterNode p | + pred instanceof SendMessage and + succ = p and + p.getParameter() instanceof AddListener + )) + } +} + +class ReturnStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + (exists (DataFlow::ParameterNode p | + succ instanceof SendMessageReturnValue and + pred = p.getAnInvocation().getArgument(0) and + p.getParameter() instanceof AddListenerReturn + )) + } +} + +class AwaitStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ){ + succ.asExpr() instanceof AwaitExpr and pred.asExpr() = succ.asExpr().(AwaitExpr).getOperand() + } +} + +} \ No newline at end of file diff --git a/javascript/lib/browserextension/BrowserInjectionObjectCustomizations.qll b/javascript/lib/browserextension/BrowserInjectionObjectCustomizations.qll new file mode 100644 index 00000000..db4302a8 --- /dev/null +++ b/javascript/lib/browserextension/BrowserInjectionObjectCustomizations.qll @@ -0,0 +1,125 @@ +/** + * Provides default sources, sinks and sanitizers for reasoning about + * Chrome API injection vulnerabilities, as well as extension points for + * adding your own. + */ +import javascript +private import browserextension.BrowserAPI + +module BrowserInjection { + + private import DataFlow::FlowLabel + /** + * A data flow source for Chrome API injection vulnerabilities. + */ + abstract class Source extends DataFlow::Node { + + + + DataFlow::FlowLabel getFlowLabel() { result = "BrowserSource" } + } + + /** + * A data flow sink for Chrome API injection vulnerabilities. + */ + abstract class Sink extends DataFlow::Node { + } + + class RemoteFlowSourceAsSource extends Source instanceof RemoteFlowSource { } + + + +/** + * Sink for chrome.tabs.update() which may allow an allow an arbitrary redirect if + * user input is used. + */ +class Update extends Sink { + Update() {exists (DataFlow::CallNode c | c = tabsRef().getAMethodCall("update") and this = c.getArgument(c.getNumArgument()-1))} + } + + class BrowserStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + (exists (DataFlow::ParameterNode p | + pred instanceof SendMessage and + succ = p and + p.getParameter() instanceof AddListener + )) + } + } + + + +/* + * A sink for chrome extensions that may allow an attacker to remove a file. + */ + +// class DownloadsRemoveFile extends Sink { +// DownloadsRemoveFile() { this = downloadsRef().getAMethodCall("removeFile") } +// } + +/** + * Sink + * chrome.bookmarks.getTree() this has no arguments + */ + + class GetTreeBookmarks extends DataFlow::Node { + GetTreeBookmarks() {bookmarksRef().getAMethodCall("getTree") = this} +} + + + + +/** + * Source + * chrome.runtime.onConnectExternal.addListener + */ + + class OnConnectExternalProxy extends Sink instanceof OnConnectExternal { + OnConnectExternalProxy() { exists(Runtime r, DataFlow::ParameterNode p| r.getAPropertyRead("onConnectExternal").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(0) = p.getParameter() and p = this)} +} + +/** +* Source +* chrome.runtime.onMessageExternal.addListener +*/ + +class OnMessageExternalProxy extends Sink instanceof OnMessageExternal { + OnMessageExternalProxy() { exists(Runtime r, DataFlow::ParameterNode p | r.getAPropertyRead("onMessageExternal").(DataFlow::SourceNode).getAMethodCall("addListener").getArgument(0).asExpr().(Function).getParameter(0) = p.getParameter() and this = p)} +} + + + + + +//Problems + +class StorageGet extends Sink { + StorageGet() {storagetypeRef().getAMethodCall("get").getAnArgument() = this} +} + +class StorageSet extends Sink { + StorageSet() {storagetypeRef().getAMethodCall("set").getAnArgument()= this} +} + +/** + * A sink for chrome extensions + * + * If a user controlled value flows into chrome.contentsettings.[contentsetting].set() an attacker + * may be able to set arbitrary settings. + */ +class SetContentSettings extends Sink { + SetContentSettings() { this = contentSettingsSettingsRef().getAMethodCall("set").getAnArgument() } +} + +/** + * A sink for chrome extensions + * + * If a user controlled value flows into chrome.contentsettings.[contentsetting].get() an attacker + * may be able to get arbitrary settings. + */ +class GetContentSettings extends Sink { + GetContentSettings() { this = contentSettingsSettingsRef().getAMethodCall("get").getAnArgument() } + } + + +} \ No newline at end of file diff --git a/javascript/lib/browserextension/CodeInjectionQuery.qll b/javascript/lib/browserextension/CodeInjectionQuery.qll new file mode 100644 index 00000000..9f0e2677 --- /dev/null +++ b/javascript/lib/browserextension/CodeInjectionQuery.qll @@ -0,0 +1,73 @@ +/** + * Provides a taint-tracking configuration for reasoning about code + * injection vulnerabilities. + * + * Note, for performance reasons: only import this file if + * `CodeInjection::Configuration` is needed, otherwise + * `CodeInjectionCustomizations` should be imported instead. + */ + + import javascript + import semmle.javascript.security.dataflow.CodeInjectionCustomizations::CodeInjection + private import BrowserAPI + private import semmle.javascript.security.dataflow.XssThroughDomCustomizations::XssThroughDom as XssThroughDom + + + + /** + * A taint-tracking configuration for reasoning about code injection vulnerabilities. + */ + class Configuration extends TaintTracking::Configuration { + Configuration() { this = "CodeInjection" } + + override predicate isSource(DataFlow::Node source) { source instanceof XssThroughDom::Source} + + + + override predicate isSink(DataFlow::Node sink) { sink instanceof Sink} + + override predicate isSanitizer(DataFlow::Node node) { + super.isSanitizer(node) or + node instanceof Sanitizer + } + + override predicate isAdditionalTaintStep(DataFlow::Node src, DataFlow::Node trg) { + // HTML sanitizers are insufficient protection against code injection + src = trg.(HtmlSanitizerCall).getInput() + } + + override predicate isAdditionalLoadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) { + exists(ExecuteScript ess | ess = pred and ess = succ and prop = ["file", "code"]) + } + } + +//Browser Extension Models +class ExecuteScriptSink extends Sink instanceof ExecuteScript{} +class ExternalConnect1 extends Source instanceof OnConnectExternal{} +class ExternalConnect2 extends Source instanceof OnMessageExternal{} + +class BrowserStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + (exists (DataFlow::ParameterNode p | + pred instanceof SendMessage and + succ = p and + p.getParameter() instanceof AddListener + )) + } +} + +class ReturnStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + (exists (DataFlow::ParameterNode p | + succ instanceof SendMessageReturnValue and + pred = p.getAnInvocation().getArgument(0) and + p.getParameter() instanceof AddListenerReturn + )) + } +} + +class AwaitStep extends DataFlow::SharedFlowStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ){ + succ.asExpr() instanceof AwaitExpr and pred.asExpr() = succ.asExpr().(AwaitExpr).getOperand() + } +} \ No newline at end of file diff --git a/javascript/src/audit/CWE-020/BrowserMessageNoVerify.ql b/javascript/src/audit/CWE-020/BrowserMessageNoVerify.ql new file mode 100644 index 00000000..ef239da6 --- /dev/null +++ b/javascript/src/audit/CWE-020/BrowserMessageNoVerify.ql @@ -0,0 +1,26 @@ +/** + * @name OnMessageExternalNoVerify + * @description Use of OnMessage Add Listener Without a Check For the ID, Origin, or URL may result in attacker data being implicitly trusted. CodeQL does not include + * manifest.json in default builds, explicitly include it in builds to use this query. This query only checks local reads, so read elsewhere may not be found. + * @kind problem + * @problem.severity warning + * @security-severity 6.1 + * @precision high + * @id js/browser-missing-origin-check + * @tags security + */ + + import javascript + import browserextension.BrowserAPI + import DataFlow + import semmle.javascript.JSON + + predicate is_externally_connectable(JsonValue res){ + res = any(JsonValue v).getPropValue("externally_connectable") + } + + from OnMessageExternalSender omes, string l + where not exists(PropRead r | DataFlow::localFlowStep*(omes, r.getBase())and r.getPropertyName() = ["id", "origin", "url"]) and + (exists(JsonValue sl | is_externally_connectable(sl) and l = sl.toString()) or + not exists(JsonValue sl | is_externally_connectable(sl)) and l = "all installed extensions") + select omes, "Unchecked external messages with user-controlled input from " + l \ No newline at end of file diff --git a/javascript/src/audit/CWE-094/BrowserExtensionCodeInjection.ql b/javascript/src/audit/CWE-094/BrowserExtensionCodeInjection.ql new file mode 100644 index 00000000..09bbe1a8 --- /dev/null +++ b/javascript/src/audit/CWE-094/BrowserExtensionCodeInjection.ql @@ -0,0 +1,24 @@ +/** + * @name Browser code injection + * @description Interpreting unsanitized user input as code allows a malicious external entity arbitrary + * code execution. + * @kind path-problem + * @problem.severity error + * @security-severity 9.3 + * @precision high + * @id js/browser-code-injection + * @tags security + * external/cwe/cwe-094 + * external/cwe/cwe-095 + * external/cwe/cwe-079 + * external/cwe/cwe-116 + */ + + import javascript + import browserextension.CodeInjectionQuery + import DataFlow::PathGraph + + from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink + where cfg.hasFlowPath(source, sink) + select sink.getNode(), source, sink, sink.getNode().(Sink).getMessagePrefix() + " depends on a $@.", + source.getNode(), "user-provided value" \ No newline at end of file diff --git a/javascript/src/audit/CWE-918/BrowserRequestForgery.ql b/javascript/src/audit/CWE-918/BrowserRequestForgery.ql new file mode 100644 index 00000000..c804635a --- /dev/null +++ b/javascript/src/audit/CWE-918/BrowserRequestForgery.ql @@ -0,0 +1,23 @@ +/** + * @name Browser request forgery + * @description Making a client-to-server request with user-controlled data in the URL allows a request forgery attack + * against the client. + * @kind path-problem + * @problem.severity error + * @security-severity 5.0 + * @precision medium + * @id js/client-side-request-forgery + * @tags security + * external/cwe/cwe-918 + */ + + import javascript + import browserextension.BothSidesRequestForgeryQuery + import DataFlow::PathGraph + + from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node request + where + cfg.hasFlowPath(source, sink) and + request = sink.getNode().(Sink).getARequest() + select request, source, sink, "The $@ of this request depends on a $@.", sink.getNode(), + sink.getNode().(Sink).getKind(), source, "user-provided value" \ No newline at end of file diff --git a/javascript/src/audit/browserAPI/BrowserInjectionFieldQuery.ql b/javascript/src/audit/browserAPI/BrowserInjectionFieldQuery.ql new file mode 100644 index 00000000..4d028cfe --- /dev/null +++ b/javascript/src/audit/browserAPI/BrowserInjectionFieldQuery.ql @@ -0,0 +1,68 @@ +/** + * @name Extension API Injection + * @description Injecting objects with attacker controlled fields into Chrome APIs may result in dangerous side effects. + * @kind path-problem + * @problem.severity warning + * @security-severity 6.1 + * @precision high + * @id js/browserapi-injection-field + * @tags security + */ + + + import javascript + import DataFlow::PathGraph + import DataFlow + import browserextension.BrowserInjectionFieldCustomizations::BrowserInjection + private import semmle.javascript.security.dataflow.XssThroughDomCustomizations::XssThroughDom as XssThroughDom + + //private import semmle.javascript.security.dataflow.DomBasedXssCustomizations + //private import semmle.javascript.security.dataflow.XssThroughDomCustomizations::XssThroughDom as XssThroughDom + + //private import semmle.javascript.security.dataflow.CodeInjectionCustomizations + + class Configuration extends TaintTracking::Configuration { + Configuration() { this = "BrowserInjection" } + + override predicate isSource(DataFlow::Node source) { + source instanceof Source + } + + override predicate isSink(DataFlow::Node sink) { + sink instanceof Sink + } + + override predicate isAdditionalLoadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) { + (pred = succ) and + ((pred instanceof Update and prop = ["url", "openerTabId"]) + or + (pred instanceof DownloadsDangerous and prop = ["body", "conflictAction","filename", "url", "method"]) + or + (pred instanceof Delete and prop = ["startTime", "endTime", "url"]) + //or + //(pred instanceof SetContentSettings and succ instanceof SetContentSettings and prop = any(string s)) + //or + //(pred instanceof GetContentSettings and succ instanceof GetContentSettings and prop = any(string s)) + //(pred instanceof StorageSet and succ instanceof StorageSet and prop = any(string s)) + //or + //(pred instanceof SearchHistory and prop = any(string s)) + or + (pred instanceof GetCookie and prop = ["domain", "firstPartyDomain", "name", "url", "session", "path", "storeId"]) + or + (pred instanceof UpdateBookmarks and prop= ["title", "url"]) + or + (pred = succ and pred instanceof RemoveBrowsingData and prop = ["cookieStoreId", "hostnames", "originTypes", "since"]) + or + (pred = succ and pred instanceof AddHistory and prop = ["url"]) + or + (pred = succ and pred instanceof CreateWindows and prop = ["url"])) + } + } + + + from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink + where cfg.hasFlowPath(source, sink) + select sink.getNode(), source, sink, sink.getNode() + " depends on a $@.", + source.getNode(), "user-provided value" + + \ No newline at end of file diff --git a/javascript/src/audit/browserAPI/BrowserInjectionObjectQuery.ql b/javascript/src/audit/browserAPI/BrowserInjectionObjectQuery.ql new file mode 100644 index 00000000..b355999e --- /dev/null +++ b/javascript/src/audit/browserAPI/BrowserInjectionObjectQuery.ql @@ -0,0 +1,64 @@ +/** + * @name Extension API Object Injection + * @description Injecting attacker controlled object into Chrome APIs may result in dangerous side effects. + * @kind path-problem + * @problem.severity warning + * @security-severity 6.1 + * @precision high + * @id js/browserapi-injection-object + * @tags security + */ + + + import javascript + import DataFlow::PathGraph + import browserextension.BrowserInjectionObjectCustomizations::BrowserInjection + import DataFlow + private import semmle.javascript.security.dataflow.XssThroughDomCustomizations::XssThroughDom as XssThroughDom + + + class ObjectLabel extends DataFlow::FlowLabel { + ObjectLabel() { + this = "Object" + } + } + + /** + * Gets either a standard flow label or the partial-taint label. + */ + DataFlow::FlowLabel anyLabel() { + result.isDataOrTaint() + } + + + class Configuration extends TaintTracking::Configuration { + Configuration() { this = "BrowserInjection" } + + override predicate isSource(DataFlow::Node source) { + source instanceof Source // optional: or source instanceof XssThroughDom::Source + } + + override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel lbl) { + sink instanceof Sink and lbl instanceof ObjectLabel + } + + override predicate isAdditionalFlowStep( + DataFlow::Node src, DataFlow::Node trg, DataFlow::FlowLabel inlbl, DataFlow::FlowLabel outlbl + ) { + // writing a tainted value to an object property makes the object tainted with ObjectLabel + exists(DataFlow::PropWrite write | + write.getRhs() = src and + inlbl = anyLabel() and + trg.(DataFlow::SourceNode).flowsTo(write.getBase()) and + outlbl instanceof ObjectLabel + ) + } + } + + + from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink + where cfg.hasFlowPath(source, sink) + select sink.getNode(), source, sink, sink.getNode() + " depends on a $@.", + source.getNode(), "user-provided value" + + \ No newline at end of file