Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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,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.
Copy link

Copilot AI Jan 2, 2026

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().

Suggested change
* Gets a reference to the Fragment module import.
* Models calls to sap.ui.core.Fragment.load() or Controller.loadFragment().

Copilot uses AI. Check for mistakes.
*/
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
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent comment style. This line uses a C++-style comment (//) while the rest of the file uses QLDoc comments (/** ... */). Consider using a QLDoc comment or block comment for consistency.

Suggested change
//ensure it is an sap module define
/* ensure it is an sap module define */

Copilot uses AI. Check for mistakes.
requiredModule.getEnclosingFunction() = sapModule.getArgument(1)
)
}

/**
* Gets the configuration object passed to Fragment.load().
*/
DataFlow::Node getConfigObject() { result = this.getArgument(0) }

/**
* Gets the 'name' property value from the config object.
* This specifies the fragment resource to load (e.g., "my.app.fragments.MyFragment").
*/
DataFlow::Node getNameArgument() {
exists(DataFlow::ObjectLiteralNode config |
config = this.getConfigObject().getALocalSource() and
result = config.getAPropertyWrite("name").getRhs()
)
}

/**
* Gets the 'controller' property value from the config object.
* This specifies the fragment's controller, if it has one.
*/
DataFlow::Node getControllerArgument() {
exists(DataFlow::ObjectLiteralNode config |
config = this.getConfigObject().getALocalSource() and
result = config.getAPropertyWrite("controller").getRhs()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abstract class UI5Fragment is defined but never used. The XmlFragment class extends UI5View instead of UI5Fragment. Either remove this unused abstract class or update XmlFragment to extend UI5Fragment if that was the original intent. If UI5Fragment is intended for future use (as mentioned in the PR description for JS fragments), consider adding a comment to clarify this.

Copilot uses AI. Check for mistakes.

/**
* A UI5 View that might include XSS sources and sinks in standard controls.
*/
Expand Down Expand Up @@ -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
Expand Down
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"
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the "use strict" directive. This should be "use strict"; for consistency and to avoid potential issues with ASI (Automatic Semicolon Insertion).

Suggested change
"use strict"
"use strict";

Copilot uses AI. Check for mistakes.
return Controller.extend("codeql-sap-js.controller.app", {
onInit: function () {
var oData = {
input: null
};
var oModel = new JSONModel(oData);
this.getView().setModel(oModel);

Fragment.load({
name: "codeql-sap-js.view.TestFragment",
controller: this,
id: this.getView().getId()
}).then(function (oFragment) {
this.getView().addDependent(oFragment);
oFragment.placeAt("fragmentContainer");
}.bind(this));
}
});
})
Copy link

Copilot AI Jan 2, 2026

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.

Suggested change
})
});

Copilot uses AI. Check for mistakes.
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");
});

});
Copy link

Copilot AI Jan 2, 2026

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.

Suggested change
});
});

Copilot uses AI. Check for mistakes.
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: &lt;img src=x onerror=alert(&quot;XSS&quot;)&gt;"
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"
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon after the "use strict" directive. This should be "use strict"; for consistency and to avoid potential issues with ASI (Automatic Semicolon Insertion).

Suggested change
"use strict"
"use strict";

Copilot uses AI. Check for mistakes.
return Controller.extend("codeql-sap-js.controller.app", {
onInit: function () {
var oData = {
input: null
};
var oModel = new JSONModel(oData);
this.getView().setModel(oModel);

this.fragment = this.loadFragment({
name: "codeql-sap-js.view.TestFragment"
}).then(function (oFragment) {
oFragment.placeAt("fragmentContainer");
}.bind(this));
}
});
})
Copy link

Copilot AI Jan 2, 2026

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.

Suggested change
})
});

Copilot uses AI. Check for mistakes.
Loading
Loading