Skip to content

Commit e09ec95

Browse files
Merge pull request #257 from advanced-security/mbaluda/sanitize-content
Ensure sanitizeContent attribute is respected
2 parents 918f8a6 + 4808163 commit e09ec95

File tree

19 files changed

+247
-91
lines changed

19 files changed

+247
-91
lines changed

.github/workflows/code_scanning.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
4343
- name: Initialize CodeQL
4444
id: initialize-codeql
45-
uses: github/codeql-action/init@v3
45+
uses: github/codeql-action/init@v4
4646
env:
4747
# Add our custom extractor to the CodeQL search path
4848
CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"init":["--search-path","${{ github.workspace }}/extractors"]}}'
@@ -62,7 +62,7 @@ jobs:
6262
6363
- name: Perform CodeQL Analysis
6464
id: analyze
65-
uses: github/codeql-action/analyze@v3
65+
uses: github/codeql-action/analyze@v4
6666
env:
6767
LGTM_INDEX_XML_MODE: all
6868
LGTM_INDEX_FILETYPES: ".json:JSON"

.github/workflows/javascript.sarif.expected

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

javascript/frameworks/ui5/ext/ui5.model.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ extensions:
9393
- ["UI5HTMLControl", "Argument[0].Member[content]", "ui5-html-injection"]
9494
- ["UI5HTMLControl", "Member[content]", "ui5-html-injection"]
9595
- ["UI5HTMLControl", "Member[setContent].Argument[0]", "ui5-html-injection"]
96+
- ["sap/ui/richtexteditor/RichTextEditor", "Member[value]", "ui5-html-injection"]
97+
- ["sap/ui/richtexteditor/RichTextEditor", "Member[setValue].Argument[0]", "ui5-html-injection"]
9698
- ["Patcher", "Member[unsafeHtml].Argument[0..]", "ui5-html-injection"]
9799
- ["RenderManager", "Member[write,unsafeHtml].Argument[0..]", "ui5-html-injection"]
98100
- ["RenderManager", "Member[writeAttribute,addStyle].Argument[0..1]", "ui5-html-injection"]

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

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import javascript
2-
import DataFlow
31
import advanced_security.javascript.frameworks.ui5.UI5
42
import advanced_security.javascript.frameworks.ui5.dataflow.DataFlow
53
private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions
@@ -648,16 +646,7 @@ class XmlView extends UI5View instanceof XmlFile {
648646
type = result.getControlTypeName() and
649647
ApiGraphModelsExtensions::sinkModel(getASuperType(type), path, "ui5-html-injection", _) and
650648
property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and
651-
result.getBindingTarget() = control.getAttribute(property) and
652-
/* If the control is an `sap.ui.core.HTML` then the control should be missing the `sanitizeContent` attribute */
653-
(
654-
getASuperType(type) = "HTMLControl"
655-
implies
656-
(
657-
not exists(control.getAttribute("sanitizeContent")) or
658-
control.getAttribute("sanitizeContent").getValue() = "false"
659-
)
660-
)
649+
result.getBindingTarget() = control.getAttribute(property)
661650
)
662651
}
663652

@@ -823,7 +812,7 @@ class UI5Control extends TUI5Control {
823812
}
824813

825814
/** Holds if this control reads from or writes to a model. */
826-
predicate accessesModel(UI5Model model) { accessesModel(model, _) }
815+
predicate accessesModel(UI5Model model) { this.accessesModel(model, _) }
827816

828817
/** Holds if this control reads from or writes to a model with regards to a binding path. */
829818
predicate accessesModel(UI5Model model, XmlBindingPath bindingPath) {
@@ -842,6 +831,43 @@ class UI5Control extends TUI5Control {
842831

843832
/** Get the controller that manages this control. */
844833
CustomController getController() { result = this.getView().getController() }
834+
835+
/**
836+
* Gets the full import path of the associated control.
837+
*/
838+
string getControlTypeName() { result = this.getQualifiedType().replaceAll(".", "/") }
839+
840+
/**
841+
* Holds if the control content is sanitized for HTML
842+
* 'sap/ui/core/HTML' sanitized using the property 'sanitizeContent'
843+
* 'sap/ui/richttexteditor/RichTextEditor' sanitized using the property 'sanitizeValue'
844+
*/
845+
predicate isHTMLSanitized() {
846+
this.getControlTypeName() = "sap/ui/richtexteditor/RichTextEditor" and
847+
not this.isSanitizePropertySetTo("sanitizeValue", false)
848+
or
849+
this.getControlTypeName() = "sap/ui/core/HTML" and
850+
this.isSanitizePropertySetTo("sanitizeContent", true) and
851+
not this.isSanitizePropertySetTo("sanitizeContent", false)
852+
}
853+
854+
bindingset[propName, val]
855+
private predicate isSanitizePropertySetTo(string propName, boolean val) {
856+
/* 1. `sanitizeContent` attribute is set declaratively. */
857+
this.getProperty(propName).toString() = val.toString()
858+
or
859+
/* 2. `sanitizeContent` attribute is set programmatically using setProperty(). */
860+
exists(CallNode node | node = this.getAReference().getAMemberCall("setProperty") |
861+
node.getArgument(0).getStringValue() = propName and
862+
not node.getArgument(1).mayHaveBooleanValue(val.booleanNot())
863+
)
864+
or
865+
/* 3. `sanitizeContent` attribute is set programmatically using a setter. */
866+
exists(CallNode node |
867+
node = this.getAReference().getAMemberCall("setS" + propName.suffix(1)) and
868+
not node.getArgument(0).mayHaveBooleanValue(val.booleanNot())
869+
)
870+
}
845871
}
846872

847873
private newtype TUI5ControlProperty =
@@ -857,7 +883,7 @@ class UI5ControlProperty extends TUI5ControlProperty {
857883
ValueNode asJsControlProperty() { this = TJsControlProperty(result) }
858884

859885
string toString() {
860-
result = this.asXmlControlProperty().toString() or
886+
result = this.asXmlControlProperty().getValue().toString() or
861887
result = this.asJsonControlProperty().toString() or
862888
result = this.asJsControlProperty().toString()
863889
}

javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import javascript
22
import semmle.javascript.dataflow.DataFlow as StdLibDataFlow
33
import advanced_security.javascript.frameworks.ui5.UI5
4-
import advanced_security.javascript.frameworks.ui5.UI5View
54
import advanced_security.javascript.frameworks.ui5.RemoteFlowSources
65
import advanced_security.javascript.frameworks.ui5.dataflow.FlowSteps
76
private import PatchDataFlow
@@ -107,6 +106,7 @@ module UI5PathGraph<PathNodeSig ConfigPathNode, PathGraphSig<ConfigPathNode> Con
107106
}
108107

109108
UI5PathNode getAPrimaryHtmlISink() {
109+
not result.asUI5BindingPathNode().getControlDeclaration().isHTMLSanitized() and
110110
if
111111
this.asDataFlowNode() instanceof LocalModelContentBoundBidirectionallyToHtmlISinkControl or
112112
this.asDataFlowNode() instanceof UI5ExternalModel // TODO: Narrow it down to ExternalModelBoundToHtmlISinkControl

javascript/frameworks/ui5/test/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ User input flows to XSS sinks via event handlers in 4 different ways:
4242

4343
### [xss-html-control](queries/UI5Xss/xss-html-control)
4444
- `sap.ui.core.HTML` Control
45+
- sanitization using the `sanitizeContent` property
46+
- sanitization disabled by programmatically setting the `sanitizeContent` property to false
4547

4648
### [xss-html-control-df](queries/UI5Xss/xss-html-control-df)
4749
- `sap.ui.core.HTML` Control
@@ -57,15 +59,17 @@ User input flows to XSS sinks via event handlers in 4 different ways:
5759

5860
### [xss-html-view](queries/UI5Xss/xss-html-view)
5961
- `sap.ui.core.mvc.HTMLView` View
60-
-
62+
6163
### [xss-indirect-control](queries/UI5Xss/xss-indirect-control)
6264
- control accessed indirectly
6365

6466
### [xss-js-view](queries/UI5Xss/xss-js-view)
6567
- `sap.ui.core.mvc.JSView` View
68+
- sanitization using the `sanitizeContent` property
6669

6770
### [xss-json-view](queries/UI5Xss/xss-json-view)
6871
- `sap.ui.core.mvc.JSONView` View
72+
- sanitization using the `sanitizeContent` property
6973

7074
### [xss-separate-renderer](queries/UI5Xss/xss-separate-renderer)
7175
- `renderer` property is set to a class name (a string)
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
| sink1.xml:5:5:5:44 | content={path: '/input'} | The binding path `content={path: '/input'}` is an HTML injection sink. |
1+
| sink1.xml:7:5:7:68 | content={path: '/input'} | The binding path `content={path: '/input'}` is an HTML injection sink. |
2+
| sink1.xml:8:5:8:44 | content={path: '/input'} | The binding path `content={path: '/input'}` is an HTML injection sink. |
3+
| sink1.xml:10:5:10:73 | value={path: '/input'} | The binding path `value={path: '/input'}` is an HTML injection sink. |

javascript/frameworks/ui5/test/models/sink/UI5ViewSinkTest.ql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ import javascript
99
import advanced_security.javascript.frameworks.ui5.UI5View
1010

1111
from UI5BindingPath bp
12-
where bp = any(UI5View ui5v).getAnHtmlISink()
12+
where
13+
bp = any(UI5View ui5v).getAnHtmlISink() and
14+
not bp.getControlDeclaration().isHTMLSanitized()
1315
select bp, "The binding path `" + bp.toString() + "` is an HTML injection sink."

javascript/frameworks/ui5/test/models/sink/logSinkTest.expected

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
| sink.js:24:38:24:42 | code0 | SAP UI5 log injection sink with kind: ui5-log-injection |
2-
| sink.js:24:45:24:49 | code1 | SAP UI5 log injection sink with kind: ui5-log-injection |
3-
| sink.js:24:52:24:56 | code2 | SAP UI5 log injection sink with kind: ui5-log-injection |
4-
| sink.js:25:38:25:42 | code0 | SAP UI5 log injection sink with kind: ui5-log-injection |
5-
| sink.js:25:45:25:49 | code1 | SAP UI5 log injection sink with kind: ui5-log-injection |
6-
| sink.js:25:52:25:56 | code2 | SAP UI5 log injection sink with kind: ui5-log-injection |
7-
| sink.js:27:40:27:44 | code0 | SAP UI5 log injection sink with kind: ui5-log-injection |
8-
| sink.js:27:47:27:51 | code1 | SAP UI5 log injection sink with kind: ui5-log-injection |
9-
| sink.js:27:54:27:58 | code2 | SAP UI5 log injection sink with kind: ui5-log-injection |
1+
| sink.js:26:38:26:42 | code0 | SAP UI5 log injection sink with kind: ui5-log-injection |
2+
| sink.js:26:45:26:49 | code1 | SAP UI5 log injection sink with kind: ui5-log-injection |
3+
| sink.js:26:52:26:56 | code2 | SAP UI5 log injection sink with kind: ui5-log-injection |
4+
| sink.js:27:38:27:42 | code0 | SAP UI5 log injection sink with kind: ui5-log-injection |
5+
| sink.js:27:45:27:49 | code1 | SAP UI5 log injection sink with kind: ui5-log-injection |
6+
| sink.js:27:52:27:56 | code2 | SAP UI5 log injection sink with kind: ui5-log-injection |
7+
| sink.js:28:40:28:44 | code0 | SAP UI5 log injection sink with kind: ui5-log-injection |
8+
| sink.js:28:47:28:51 | code1 | SAP UI5 log injection sink with kind: ui5-log-injection |
9+
| sink.js:28:54:28:58 | code2 | SAP UI5 log injection sink with kind: ui5-log-injection |
1010
| sink.js:29:37:29:41 | code0 | SAP UI5 log injection sink with kind: ui5-log-injection |
1111
| sink.js:29:44:29:48 | code1 | SAP UI5 log injection sink with kind: ui5-log-injection |
1212
| sink.js:29:51:29:55 | code2 | SAP UI5 log injection sink with kind: ui5-log-injection |

javascript/frameworks/ui5/test/models/sink/sink.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ sap.ui.define(
88
"sap/base/util/Properties",
99
"sap/ui/core/RenderManager",
1010
"sap/ui/util/Storage",
11-
"sap/ui/core/util/File"
11+
"sap/ui/core/util/File",
12+
"sap/ui/richtexteditor/RichTextEditor"
1213
],
1314
function (
1415
LoaderExtensions,
@@ -19,13 +20,12 @@ sap.ui.define(
1920
Properties,
2021
RenderManager,
2122
Storage,
22-
File
23+
File,
24+
RichTextEditor
2325
) {
2426
var value = jQuery.sap.log.fatal(code0, code1, code2);
2527
var value = jQuery.sap.log.error(code0, code1, code2);
26-
2728
var value = jQuery.sap.log.warning(code0, code1, code2);
28-
2929
var value = jQuery.sap.log.info(code0, code1, code2);
3030

3131
var value = jQuery.sap.log.debug(code0, code1, code2);
@@ -94,7 +94,7 @@ sap.ui.define(
9494
var value = obj.registerResourcePath(code0, code1);
9595
var obj = new Properties();
9696
var value = obj.create(code0);
97-
var obj = new HTML({content: code0});
97+
var obj = new HTML({ content: code0 }); // UNSAFE: Content is not sanitized
9898
obj.content = code0;
9999
obj.setContent(code0);
100100
var obj = new Patcher();
@@ -125,5 +125,12 @@ sap.ui.define(
125125
var value = sap.ui.core.util.File.save(code0, code1, "csv", "text/csv", code4, code5);
126126
var value = sap.ui.core.util.File.save(code0, code1, "csv", "text/plain", code4, code5);
127127
var value = sap.ui.core.util.File.save(code0, code1, code2, code3, code4, code5);
128+
129+
var obj = new HTML({ content: code0, sanitizeContent: true }); // SAFE: Content is sanitized
130+
var obj = new HTML({ content: code0, sanitizeContent: false }); // UNSAFE: Content is explicitly not sanitized
131+
132+
var obj = new RichTextEditor({ value: code0 }); // SAFE: Content is sanitized by default
133+
var obj = new RichTextEditor({ value: code0, sanitizeValue: true }); // SAFE: Content is sanitized
134+
var obj = new RichTextEditor({ value: code0, sanitizeValue: false }); // UNSAFE: Content is explicitly not sanitized
128135
},
129136
);

0 commit comments

Comments
 (0)