Skip to content

Commit 3c580c3

Browse files
committed
Fix cross-app isolation for UI5 default OData model
This commit: - fixes cross-app isolation for the edge case where multiple UI5 apps are under the same "source-root" directory -- and are therefore analyzed via the same CodeQL database -- while also using the default OData model via bindElement. - adds expected js/ui5-xss results for the new xss-cross-app-isolation unit test: - webapp_a/fragments/DataDisplay.fragment.xml (line 7) - webapp_b/fragments/DataDisplay.fragment.xml (line 7) These results validate that the improved DefaultODataServiceModel class correctly isolates XSS detection within webapp boundaries when using default OData models with bindElement.
1 parent a6964d5 commit 3c580c3

22 files changed

Lines changed: 331 additions & 7 deletions

File tree

.github/workflows/javascript.sarif.expected

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,15 @@ abstract class UI5ExternalModel extends UI5Model, RemoteFlowSource {
113113
/** Default model which gains content from an SAP OData service (ie no model name is explicitly specified). */
114114
class DefaultODataServiceModel extends UI5ExternalModel {
115115
DefaultODataServiceModel() {
116-
exists(ExternalModelManifest model |
116+
exists(ExternalModelManifest model, WebApp webapp |
117117
//an OData default model exists
118118
model.getName() = "" and
119119
model.getDataSource() instanceof ODataDataSourceManifest and
120120
//therefore the bindElement calls that exist may be sources and also approximates the model itself
121-
this.getCalleeName() = "bindElement"
121+
this.getCalleeName() = "bindElement" and
122+
// The bindElement call must be in the same webapp as the manifest that declares the default model
123+
webapp.getAResource() = this.getFile() and
124+
webapp.getManifest() = model.getJsonFile()
122125
)
123126
}
124127

javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -944,16 +944,23 @@ module ManifestJson {
944944
exists(JsonObject models |
945945
this = models.getPropValue(modelName) and
946946
dataSourceName = this.getPropStringValue("dataSource") and
947-
/* This data source can be found in the "dataSources" property */
948-
exists(DataSourceManifest dataSource | dataSource.getName() = dataSourceName)
947+
/* This data source can be found in the "dataSources" property of the same manifest */
948+
exists(DataSourceManifest dataSource |
949+
dataSource.getName() = dataSourceName and
950+
dataSource.getManifestJson() = this.getJsonFile()
951+
)
949952
)
950953
}
951954

952955
string getName() { result = modelName }
953956

954957
string getDataSourceName() { result = dataSourceName }
955958

956-
DataSourceManifest getDataSource() { result.getName() = dataSourceName }
959+
/** Gets the data source for this external model from the same manifest file. */
960+
DataSourceManifest getDataSource() {
961+
result.getName() = dataSourceName and
962+
result.getManifestJson() = this.getJsonFile()
963+
}
957964
}
958965

959966
class ManifestJson extends File {

javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5View.qll

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ abstract class UI5BindingPath extends BindingPath {
150150
load.getNameArgument()
151151
.getStringValue()
152152
.matches("%" +
153-
this.getLocation().getFile().getBaseName().replaceAll(".fragment.xml", "") + "%")
153+
this.getLocation().getFile().getBaseName().replaceAll(".fragment.xml", "") + "%") and
154+
// The fragment load call must be in the same webapp as the fragment file
155+
inSameWebApp(this.getLocation().getFile(), load.getFile())
154156
)
155157
)
156158
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
nodes
2+
| webapp_a/controller/App.controller.js:21:17:21:54 | oFragme ... sA(1)") |
3+
| webapp_a/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageA} |
4+
| webapp_b/controller/App.controller.js:21:17:21:54 | oFragme ... sB(1)") |
5+
| webapp_b/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageB} |
6+
edges
7+
| webapp_a/controller/App.controller.js:21:17:21:54 | oFragme ... sA(1)") | webapp_a/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageA} |
8+
| webapp_a/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageA} | webapp_a/controller/App.controller.js:21:17:21:54 | oFragme ... sA(1)") |
9+
| webapp_b/controller/App.controller.js:21:17:21:54 | oFragme ... sB(1)") | webapp_b/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageB} |
10+
| webapp_b/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageB} | webapp_b/controller/App.controller.js:21:17:21:54 | oFragme ... sB(1)") |
11+
#select
12+
| webapp_a/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageA} | webapp_a/controller/App.controller.js:21:17:21:54 | oFragme ... sA(1)") | webapp_a/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageA} | XSS vulnerability due to $@. | webapp_a/controller/App.controller.js:21:17:21:54 | oFragme ... sA(1)") | user-provided value |
13+
| webapp_b/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageB} | webapp_b/controller/App.controller.js:21:17:21:54 | oFragme ... sB(1)") | webapp_b/fragments/DataDisplay.fragment.xml:7:9:7:43 | content={messageB} | XSS vulnerability due to $@. | webapp_b/controller/App.controller.js:21:17:21:54 | oFragme ... sB(1)") | user-provided value |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
UI5Xss/UI5Xss.ql
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "xss-cross-app-isolation",
3+
"version": "1.0.0",
4+
"description": "Test case for ensuring XSS detection does not cross webapp boundaries with default OData models"
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
specVersion: "3.1"
2+
type: application
3+
metadata:
4+
name: xss-cross-app-isolation
5+
framework:
6+
name: SAPUI5
7+
version: "1.120.0"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
sap.ui.define([
2+
"sap/ui/core/UIComponent"
3+
], function (UIComponent) {
4+
"use strict";
5+
return UIComponent.extend("app.a.Component", {
6+
metadata: {
7+
manifest: "json"
8+
},
9+
init: function () {
10+
UIComponent.prototype.init.apply(this, arguments);
11+
}
12+
});
13+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
sap.ui.define([
2+
"sap/ui/core/mvc/Controller",
3+
"sap/ui/core/Fragment"
4+
], function (Controller, Fragment) {
5+
"use strict";
6+
7+
return Controller.extend("app.a.controller.App", {
8+
onInit: function () {
9+
// XSS vulnerability pattern in App A:
10+
// 1. OData model is configured as default model in manifest.json
11+
// 2. Fragment is loaded dynamically
12+
// 3. Fragment is bound to OData entity via bindElement
13+
// 4. Fragment contains HTML control that renders OData content
14+
15+
Fragment.load({
16+
id: this.getView().getId(),
17+
name: "app.a.fragments.DataDisplay",
18+
controller: this
19+
}).then(function (oFragment) {
20+
// Bind fragment to an OData entity - vulnerability is in the backend data
21+
oFragment.bindElement("/MessagesA(1)");
22+
23+
// Add the fragment to the page content
24+
this.byId("page").addContent(oFragment);
25+
}.bind(this));
26+
}
27+
});
28+
});

0 commit comments

Comments
 (0)