-
Notifications
You must be signed in to change notification settings - Fork 3
Add small investigative modelling for fragments - WIP #270
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
base: main
Are you sure you want to change the base?
Changes from all commits
02c8eda
ad42e91
8f80775
b0c1aef
97af1ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||
| /** | ||||||
| * Provides classes for modeling the sap.ui.core.Fragment API. | ||||||
| */ | ||||||
|
|
||||||
| import javascript | ||||||
| import DataFlow | ||||||
| import advanced_security.javascript.frameworks.ui5.UI5 | ||||||
|
|
||||||
| /** | ||||||
| * Gets a reference to the Fragment module import. | ||||||
| */ | ||||||
| class FragmentLoad extends InvokeNode, MethodCallNode { | ||||||
| FragmentLoad() { | ||||||
| this = | ||||||
| TypeTrackers::hasDependency(["sap/ui/core/Fragment", "sap.ui.core.Fragment"]) | ||||||
| .getAMemberCall("load") | ||||||
| or | ||||||
| exists(RequiredObject requiredModule, SapDefineModule sapModule | | ||||||
| this = requiredModule.asSourceNode().getAMemberCall("load") and | ||||||
| //ensure it is an sap module define | ||||||
|
||||||
| //ensure it is an sap module define | |
| /* ensure it is an sap module define */ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import advanced_security.javascript.frameworks.ui5.UI5 | |
| import advanced_security.javascript.frameworks.ui5.dataflow.DataFlow | ||
| private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions | ||
| import advanced_security.javascript.frameworks.ui5.Bindings | ||
| import advanced_security.javascript.frameworks.ui5.Fragment | ||
|
|
||
| /** | ||
| * Gets the immediate supertype of a given type from the extensible predicate `typeModel` provided by | ||
|
|
@@ -185,6 +186,17 @@ predicate isBuiltInControl(string qualifiedTypeUri) { | |
| ) | ||
| } | ||
|
|
||
| /** | ||
| * A UI5 Fragment that might include XSS sources and sinks in standard controls. | ||
| */ | ||
| abstract class UI5Fragment extends File { | ||
| abstract UI5Control getControl(); | ||
|
|
||
| abstract UI5BindingPath getASource(); | ||
|
|
||
| abstract UI5BindingPath getAnHtmlISink(); | ||
| } | ||
|
Comment on lines
+189
to
+198
|
||
|
|
||
| /** | ||
| * A UI5 View that might include XSS sources and sinks in standard controls. | ||
| */ | ||
|
|
@@ -672,8 +684,106 @@ class XmlView extends UI5View instanceof XmlFile { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * An xml fragment. It may or may not have controllers associated. | ||
| */ | ||
| class XmlFragment extends UI5View instanceof XmlFile { | ||
| XmlRootElement root; | ||
|
|
||
| XmlFragment() { | ||
| root = this.getARootElement() and | ||
| ( | ||
| root.getNamespace().getUri() = "sap.m" | ||
| or | ||
| root.getNamespace().getUri() = "sap.ui.core" | ||
| ) and | ||
| root.hasName("FragmentDefinition") | ||
| } | ||
|
|
||
| override XmlBindingPath getASource() { | ||
| exists(XmlElement control, string type, string path, string property | | ||
| type = result.getControlTypeName() and | ||
| this = control.getFile() and | ||
| ApiGraphModelsExtensions::sourceModel(getASuperType(type), path, "remote", _) and | ||
| property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and | ||
| result.getBindingTarget() = control.getAttribute(property) | ||
| ) | ||
| } | ||
|
|
||
| override XmlBindingPath getAnHtmlISink() { | ||
| exists(XmlElement control, string type, string path, string property | | ||
| this = control.getFile() and | ||
| type = result.getControlTypeName() and | ||
| ApiGraphModelsExtensions::sinkModel(getASuperType(type), path, "ui5-html-injection", _) and | ||
| property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and | ||
| result.getBindingTarget() = control.getAttribute(property) and | ||
| /* If the control is an `sap.ui.core.HTML` then the control should be missing the `sanitizeContent` attribute */ | ||
| ( | ||
| getASuperType(type) = "HTMLControl" | ||
| implies | ||
| ( | ||
| not exists(control.getAttribute("sanitizeContent")) or | ||
| control.getAttribute("sanitizeContent").getValue() = "false" | ||
| ) | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| override UI5Control getControl() { | ||
| exists(XmlElement element | | ||
| result.asXmlControl() = element and | ||
| /* Use getAChild+ because some controls nest other controls inside them as aggregations */ | ||
| element = root.getAChild+() and | ||
| ( | ||
| /* 1. A builtin control provided by UI5 */ | ||
| isBuiltInControl(element.getNamespace().getUri()) | ||
| or | ||
| /* 2. A custom control with implementation code found in the webapp */ | ||
| exists(CustomControl control | | ||
| control.getName() = element.getNamespace().getUri() + "." + element.getName() and | ||
| inSameWebApp(control.getFile(), element.getFile()) | ||
| ) | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * This is either known in the location from which `loadFragment` is called (in a controller's init function) | ||
| * OR in the optional controller param of `Fragment.load`. | ||
| * This MAY return no value, when the fragment is not associated to any controller. | ||
| * When this returns a value it is guaranteed that this xml fragment is instantiated. | ||
| */ | ||
| override string getControllerName() { | ||
| exists(CustomController controller, MethodCallNode loadFragmentCall | | ||
| loadFragmentCall.getMethodName() = "loadFragment" and | ||
| controller.getAThisNode().flowsTo(loadFragmentCall.getReceiver()) and | ||
| controller.getName() = result | ||
| ) | ||
| or | ||
| exists(CustomController controller, FragmentLoad fragmentLoad | | ||
| controller.getAThisNode().flowsTo(fragmentLoad.getControllerArgument()) and | ||
| /* | ||
| * extracting just the base name of the fragment (not the fully qualified) | ||
| * otherwise difficult to know which part of absolute path is only for the qualified name | ||
| */ | ||
|
|
||
| fragmentLoad | ||
| .getNameArgument() | ||
| .getStringValue() | ||
| .matches("%" + this.getBaseName().replaceAll(".fragment.xml", "")) and | ||
| controller.getName() = result | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private newtype TUI5Control = | ||
| TXmlControl(XmlElement control) or | ||
| TXmlControl(XmlElement control) { | ||
| control | ||
| .(Locatable) | ||
| .getFile() | ||
| .getBaseName() | ||
| .matches(["%.view.xml", "%.view.html", "%.fragment.xml"]) | ||
| } or | ||
| TJsonControl(JsonObject control) { | ||
| exists(JsonView view | control.getParent() = view.getRoot().getPropValue("content")) | ||
| } or | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| nodes | ||
| | webapp/controller/app.controller.js:10:17:10:27 | input: null | | ||
| | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | | ||
| | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | | ||
| edges | ||
| | webapp/controller/app.controller.js:10:17:10:27 | input: null | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | | ||
| | webapp/controller/app.controller.js:10:17:10:27 | input: null | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | | ||
| | webapp/controller/app.controller.js:12:26:12:45 | new JSONModel(oData) | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | | ||
| | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | webapp/controller/app.controller.js:10:17:10:27 | input: null | | ||
| | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | webapp/controller/app.controller.js:12:26:12:45 | new JSONModel(oData) | | ||
| | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | webapp/controller/app.controller.js:10:17:10:27 | input: null | | ||
| #select | ||
| | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | XSS vulnerability due to $@. | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | user-provided value | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| UI5Xss/UI5Xss.ql |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "sap-ui5-xss", | ||
| "version": "1.0.0", | ||
| "main": "index.js" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| specVersion: '3.0' | ||
| metadata: | ||
| name: sap-ui5-xss | ||
| type: application | ||
| framework: | ||
| name: SAPUI5 | ||
| version: "1.115.0" |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||
| sap.ui.define([ | ||||||
| "sap/ui/core/mvc/Controller", | ||||||
| "sap/ui/model/json/JSONModel", | ||||||
| "sap/ui/core/Fragment" | ||||||
| ], function (Controller, JSONModel, Fragment) { | ||||||
| "use strict" | ||||||
|
||||||
| "use strict" | |
| "use strict"; |
Copilot
AI
Jan 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing semicolon after the closing parenthesis of the function call. The statement should end with a semicolon for consistency with JavaScript best practices.
| }) | |
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <!DOCTYPE html> | ||
| <html> | ||
|
|
||
| <head> | ||
|
|
||
| <meta charset="utf-8"> | ||
| <title>SAPUI5 XSS</title> | ||
| <script src="https://sdk.openui5.org/resources/sap-ui-core.js" | ||
| data-sap-ui-libs="sap.m" | ||
| data-sap-ui-onInit="module:codeql-sap-js/index" | ||
| data-sap-ui-resourceroots='{ | ||
| "codeql-sap-js": "./" | ||
| }'> | ||
| </script> | ||
| </head> | ||
|
|
||
| <body class="sapUiBody" id="content"> | ||
|
|
||
| </body> | ||
|
|
||
| </html> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,11 @@ | ||||||
| sap.ui.define([ | ||||||
| "sap/ui/core/mvc/HTMLView" | ||||||
| ], function (HTMLView) { | ||||||
| "use strict"; | ||||||
| HTMLView.create({ | ||||||
| viewName: "codeql-sap-js.view.app" | ||||||
| }).then(function (oView) { | ||||||
| oView.placeAt("content"); | ||||||
| }); | ||||||
|
|
||||||
| }); | ||||||
|
||||||
| }); | |
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "sap.app": { | ||
| "id": "sap-ui5-xss" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <core:FragmentDefinition | ||
| xmlns="sap.m" | ||
| xmlns:core="sap.ui.core" | ||
| > | ||
| <Input xmlns="sap.m" | ||
| description="Enter Payload - Try: <img src=x onerror=alert("XSS")>" | ||
| value="{/input}"> <!--User input source sap.m.Input.value --> | ||
| </Input> | ||
| </core:FragmentDefinition> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| <template data-controller-name="codeql-sap-js.controller.app"> | ||
| <div id="fragmentContainer"></div> | ||
| <div data-sap-ui-type="sap.ui.core.HTML" | ||
| data-content="{/input}"> <!--XSS sink sap.ui.core.HTML.content --> | ||
|
||
| </div> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| nodes | ||
| | webapp/controller/app.controller.js:9:17:9:27 | input: null | | ||
| | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | | ||
| | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | | ||
| edges | ||
| | webapp/controller/app.controller.js:9:17:9:27 | input: null | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | | ||
| | webapp/controller/app.controller.js:9:17:9:27 | input: null | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | | ||
| | webapp/controller/app.controller.js:11:26:11:45 | new JSONModel(oData) | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | | ||
| | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | webapp/controller/app.controller.js:9:17:9:27 | input: null | | ||
| | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | webapp/controller/app.controller.js:11:26:11:45 | new JSONModel(oData) | | ||
| | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | webapp/controller/app.controller.js:9:17:9:27 | input: null | | ||
| #select | ||
| | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | webapp/view/app.view.html:4:11:4:33 | data-content={/input} | XSS vulnerability due to $@. | webapp/view/TestFragment.fragment.xml:5:1:7:28 | value={/input} | user-provided value | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| UI5Xss/UI5Xss.ql |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "sap-ui5-xss", | ||
| "version": "1.0.0", | ||
| "main": "index.js" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| specVersion: '3.0' | ||
| metadata: | ||
| name: sap-ui5-xss | ||
| type: application | ||
| framework: | ||
| name: SAPUI5 | ||
| version: "1.115.0" |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||
| sap.ui.define([ | ||||||
| "sap/ui/core/mvc/Controller", | ||||||
| "sap/ui/model/json/JSONModel" | ||||||
| ], function (Controller, JSONModel) { | ||||||
| "use strict" | ||||||
|
||||||
| "use strict" | |
| "use strict"; |
Copilot
AI
Jan 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing semicolon after the closing parenthesis of the function call. The statement should end with a semicolon for consistency with JavaScript best practices.
| }) | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation comment says "Gets a reference to the Fragment module import" but the class name is FragmentLoad, which represents a Fragment.load() call rather than just a module import. The comment should be updated to clarify that this class models calls to Fragment.load() or Controller.loadFragment().