Skip to content

Commit 00575e7

Browse files
authored
Merge pull request #283 from advanced-security/dd/default-odata-model-fragments
Improve `UI5Xss.ql` query to detect default OData model with `bindElement`
2 parents 003cfdb + e0ed82a commit 00575e7

File tree

34 files changed

+515
-11
lines changed

34 files changed

+515
-11
lines changed

.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/Bindings.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,4 +748,9 @@ class Binding extends TBinding {
748748
BindingPath getBindingPath() { result.getBinding() = this }
749749

750750
BindingTarget getBindingTarget() { result.getBinding() = this }
751+
752+
/**
753+
* Gets the `BindElementMethodCallNode` for this binding, if it is a context binding via `bindElement`.
754+
*/
755+
BindElementMethodCallNode getBindElementCall() { this = TLateJavaScriptContextBinding(result, _) }
751756
}

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,26 @@ abstract class UI5ExternalModel extends UI5Model, RemoteFlowSource {
114114
class DefaultODataServiceModel extends UI5ExternalModel {
115115
DefaultODataServiceModel() {
116116
exists(ExternalModelManifest model |
117-
//an OData default model exists
117+
// An OData default model exists.
118118
model.getName() = "" and
119119
model.getDataSource() instanceof ODataDataSourceManifest and
120-
//therefore the bindElement calls that exist may be sources and also approximates the model itself
121-
this.getCalleeName() = "bindElement"
120+
// A bindElement call bound to the default OData model represents a source of data.
121+
this.getCalleeName() = "bindElement" and
122+
// The bindElement call must be in the same webapp as the manifest that declares the default model.
123+
inSameWebApp(this.getFile(), model.getJsonFile())
122124
)
123125
}
124126

125127
override string getSourceType() { result = "DefaultODataServiceModel" }
126128

127129
override string getName() { result = "" }
128130

129-
Binding asBinding() { result.getBindingTarget().asDataFlowNode() = this }
131+
/**
132+
* Gets bindings associated with this default OData model source.
133+
* Since `DefaultODataServiceModel` represents a `bindElement` call,
134+
* we match context bindings whose `bindElement` call is this node.
135+
*/
136+
Binding asBinding() { result.getBindElementCall() = this }
130137
}
131138

132139
/** Model which gains content from an SAP OData service. */

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ bindingset[f1, f2]
8888
pragma[inline_late]
8989
predicate inSameWebApp(File f1, File f2) {
9090
exists(WebApp webApp | webApp.getAResource() = f1 and webApp.getAResource() = f2)
91+
or
92+
exists(WebApp webApp | webApp.getManifest() = f1 and webApp.getAResource() = f2)
93+
or
94+
exists(WebApp webApp | webApp.getManifest() = f2 and webApp.getAResource() = f1)
9195
}
9296

9397
/** A UI5 bootstrapped web application. */
@@ -944,16 +948,23 @@ module ManifestJson {
944948
exists(JsonObject models |
945949
this = models.getPropValue(modelName) and
946950
dataSourceName = this.getPropStringValue("dataSource") and
947-
/* This data source can be found in the "dataSources" property */
948-
exists(DataSourceManifest dataSource | dataSource.getName() = dataSourceName)
951+
/* This data source can be found in the "dataSources" property of the same manifest */
952+
exists(DataSourceManifest dataSource |
953+
dataSource.getName() = dataSourceName and
954+
dataSource.getManifestJson() = this.getJsonFile()
955+
)
949956
)
950957
}
951958

952959
string getName() { result = modelName }
953960

954961
string getDataSourceName() { result = dataSourceName }
955962

956-
DataSourceManifest getDataSource() { result.getName() = dataSourceName }
963+
/** Gets the data source for this external model from the same manifest file. */
964+
DataSourceManifest getDataSource() {
965+
result.getName() = dataSourceName and
966+
result.getManifestJson() = this.getJsonFile()
967+
}
957968
}
958969

959970
class ManifestJson extends File {

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,13 @@ abstract class UI5BindingPath extends BindingPath {
133133
not exists(this.getModelName())
134134
)
135135
or
136-
/* 5. There is no call to `setModel` at all and a default model exists that is related to the binding path this refers to */
136+
/* 5. There is no call to `setModel` in the same webapp and a default model exists that is related to the binding path this refers to */
137137
exists(DefaultODataServiceModel defaultModel |
138138
result = defaultModel and
139-
not exists(MethodCallNode viewSetModelCall | viewSetModelCall.getMethodName() = "setModel") and
139+
not exists(MethodCallNode viewSetModelCall |
140+
viewSetModelCall.getMethodName() = "setModel" and
141+
inSameWebApp(this.getLocation().getFile(), viewSetModelCall.getFile())
142+
) and
140143
/*
141144
* this binding path can occur in a fragment that is the receiver object for the bindElement model approximation
142145
* i.e. checks that the default model is relevant
@@ -147,7 +150,9 @@ abstract class UI5BindingPath extends BindingPath {
147150
load.getNameArgument()
148151
.getStringValue()
149152
.matches("%" +
150-
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())
151156
)
152157
)
153158
)
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+
});

0 commit comments

Comments
 (0)