Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d52a0c7
Ensure sanitizeContent attribute is respected
mbaluda Nov 19, 2025
1ec8bfa
Ensure sanitizeContent attribute is respected
mbaluda Nov 19, 2025
0a7b668
Add more tests
mbaluda Nov 19, 2025
6a00000
Exclude sanitized HTML views from XSS sinks
mbaluda Nov 21, 2025
807ea34
Fix ql-4-ql alerts
mbaluda Nov 21, 2025
02c4537
Fix workflow
mbaluda Nov 21, 2025
235c441
Update expected file
mbaluda Nov 21, 2025
f108236
fix README
mbaluda Nov 21, 2025
ee49547
Address review comments
mbaluda Nov 25, 2025
9b50447
Fix conflict
mbaluda Nov 30, 2025
ddf7a6e
Merge branch 'main' into mbaluda/sanitize-content
mbaluda Nov 30, 2025
1a2c5cc
update expected sile
mbaluda Nov 30, 2025
0929f6e
fix expected file
mbaluda Nov 30, 2025
6baa2d7
Merge branch 'main' into mbaluda/sanitize-content
mbaluda Dec 1, 2025
744bf21
Update javascript/frameworks/ui5/lib/advanced_security/javascript/fra…
mbaluda Dec 2, 2025
9a36d3d
improved sink tests
mbaluda Dec 3, 2025
6d62945
update ui5.model.yml with RTE sinks
mbaluda Dec 3, 2025
b301afd
rename function isHTMLSanitized
mbaluda Dec 3, 2025
41ac329
Fix sink test for RichTextEditor
mbaluda Dec 3, 2025
213e17a
tests
mbaluda Dec 3, 2025
0b88cb8
Merge branch 'main' into mbaluda/sanitize-content
mbaluda Dec 8, 2025
bb2aab5
Address review
mbaluda Dec 8, 2025
8072f6b
missing tag
mbaluda Dec 8, 2025
e2bf1b3
Merge branch 'main' into mbaluda/sanitize-content
jeongsoolee09 Dec 11, 2025
863ea7b
Address review 2
mbaluda Dec 11, 2025
03ebe7f
Merge branch 'main' into mbaluda/sanitize-content
mbaluda Dec 11, 2025
4808163
update expected file
mbaluda Dec 12, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/code_scanning.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

- name: Initialize CodeQL
id: initialize-codeql
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
env:
# Add our custom extractor to the CodeQL search path
CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"init":["--search-path","${{ github.workspace }}/extractors"]}}'
Expand All @@ -62,7 +62,7 @@ jobs:

- name: Perform CodeQL Analysis
id: analyze
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
env:
LGTM_INDEX_XML_MODE: all
LGTM_INDEX_FILETYPES: ".json:JSON"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/javascript.sarif.expected

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import DataFlow
import advanced_security.javascript.frameworks.ui5.JsonParser
import semmle.javascript.security.dataflow.DomBasedXssCustomizations
import advanced_security.javascript.frameworks.ui5.UI5View
import advanced_security.javascript.frameworks.ui5.UI5Control
import advanced_security.javascript.frameworks.ui5.UI5HTML
import codeql.util.FileSystem
private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import advanced_security.javascript.frameworks.ui5.UI5

private newtype TUI5Control =
TXmlControl(XmlElement control) or
TJsonControl(JsonObject control) {
exists(JsonView view | control.getParent() = view.getRoot().getPropValue("content"))
} or
TJsControl(NewNode control) {
exists(JsView view |
control.asExpr().getParentExpr() =
view.getRoot()
.getArgument(1)
.getALocalSource()
.(ObjectLiteralNode)
.getAPropertyWrite("createContent")
.getRhs()
.(FunctionNode)
.getReturnNode()
.getALocalSource()
.(ArrayLiteralNode)
.asExpr()
)
}

class UI5Control extends TUI5Control {
XmlElement asXmlControl() { this = TXmlControl(result) }

JsonObject asJsonControl() { this = TJsonControl(result) }

NewNode asJsControl() { this = TJsControl(result) }

string toString() {
result = this.asXmlControl().toString()
or
result = this.asJsonControl().toString()
or
result = this.asJsControl().toString()
}

predicate hasLocationInfo(
string filepath, int startcolumn, int startline, int endcolumn, int endline
) {
this.asXmlControl().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
or
/* Since JsonValue does not implement `hasLocationInfo`, we use `getLocation` instead. */
exists(Location location | location = this.asJsonControl().getLocation() |
location.getFile().getAbsolutePath() = filepath and
location.getStartColumn() = startcolumn and
location.getStartLine() = startline and
location.getEndColumn() = endcolumn and
location.getEndLine() = endline
)
or
this.asJsControl().hasLocationInfo(filepath, startcolumn, startline, endcolumn, endline)
}

/**
* Gets the qualified type string, e.g. `sap.m.SearchField`.
*/
string getQualifiedType() {
exists(XmlElement control | control = this.asXmlControl() |
result = control.getNamespace().getUri() + "." + control.getName()
)
or
exists(JsonObject control | control = this.asJsonControl() |
result = control.getPropStringValue("Type")
)
or
exists(NewNode control | control = this.asJsControl() |
result = this.asJsControl().asExpr().getAChildExpr().(DotExpr).getQualifiedName()
)
}

File getFile() {
result = this.asXmlControl().getFile() or
result = this.asJsonControl().getFile() or
result = this.asJsControl().getFile()
}

/**
* Gets the `id` property of this control.
*/
string getId() { result = this.getProperty("id").getValue() }

/**
* Gets the qualified type name, e.g. `sap/m/SearchField`.
*/
string getImportPath() { result = this.getQualifiedType().replaceAll(".", "/") }

/**
* Gets the definition of this control if this is a custom one.
*/
CustomControl getDefinition() {
result.getName() = this.getQualifiedType() and
inSameWebApp(this.getFile(), result.getFile())
}

/**
* Gets a reference to this control. Currently supports only such references made through `byId`.
*/
ControlReference getAReference() {
result.getMethodName() = "byId" and
result.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() =
this.getProperty("id").getValue()
}

/** Gets a property of this control having the name. */
UI5ControlProperty getProperty(string propName) {
result.asXmlControlProperty() = this.asXmlControl().getAttribute(propName)
or
result.asJsonControlProperty() = this.asJsonControl().getPropValue(propName)
or
result.asJsControlProperty() =
this.asJsControl()
.getArgument(0)
.getALocalSource()
.asExpr()
.(ObjectExpr)
.getPropertyByName(propName)
.getAChildExpr()
.flow() and
not exists(Property property | result.asJsControlProperty() = property.getNameExpr().flow())
}

/** Gets a property of this control. */
UI5ControlProperty getAProperty() { result = this.getProperty(_) }

bindingset[propName]
MethodCallNode getARead(string propName) {
// TODO: in same view
inSameWebApp(this.getFile(), result.getFile()) and
result.getMethodName() = "get" + capitalize(propName)
}

bindingset[propName]
MethodCallNode getAWrite(string propName) {
// TODO: in same view
inSameWebApp(this.getFile(), result.getFile()) and
result.getMethodName() = "set" + capitalize(propName)
}

/** Holds if this control reads from or writes to a model. */
predicate accessesModel(UI5Model model) { this.accessesModel(model, _) }

/** Holds if this control reads from or writes to a model with regards to a binding path. */
predicate accessesModel(UI5Model model, XmlBindingPath bindingPath) {
// Verify that the controller's model has the referenced property
exists(XmlView view |
// Both this control and the model belong to the same view
this = view.getControl() and
model = view.getController().getModel() and
model.(UI5InternalModel).getPathString() = bindingPath.getPath() and
bindingPath.getBindingTarget() = this.asXmlControl().getAnAttribute()
)
}

/** Get the view that this control is part of. */
UI5View getView() { result = this.asXmlControl().getFile() }

/** Get the controller that manages this control. */
CustomController getController() { result = this.getView().getController() }

/**
* Gets the full import path of the associated control.
*/
string getControlTypeName() { result = this.getQualifiedType().replaceAll(".", "/") }

/**
* Holds if the attribute `sanitizeContent`
* in controls `sap.ui.core.HTML` and `sap.ui.richttexteditor.RichTextEditor`
* is set to true and never set to false anywhere
*/
predicate isSanitizedControl() {
not this = this.sanitizeContentSetTo(false) and
(
this.getControlTypeName() = "sap/ui/richttexteditor/RichTextEditor"
or
this.getControlTypeName() = "sap/ui/core/HTML" and
this = this.sanitizeContentSetTo(true)
)
}

bindingset[val]
private UI5Control sanitizeContentSetTo(boolean val) {
// `sanitizeContent` attribute is set declaratively
result.getProperty("sanitizeContent").toString() = val.toString()
or
//or
// `sanitizeContent` attribute is set programmatically (not sufficient)
//result
// .getAReference()
// .hasPropertyWrite("sanitizeContent",
// any(DataFlow::Node n | not n.mayHaveBooleanValue(val.booleanNot())))
// `sanitizeContent` attribute is set programmatically using setProperty()
exists(CallNode node | node = result.getAReference().getAMemberCall("setProperty") |
node.getArgument(0).getStringValue() = "sanitizeContent" and
not node.getArgument(1).mayHaveBooleanValue(val.booleanNot())
)
}
}

class SanitizedUI5Control extends UI5Control {
SanitizedUI5Control() { super.isSanitizedControl() }
}

private newtype TUI5ControlProperty =
TXmlControlProperty(XmlAttribute property) or
TJsonControlProperty(JsonValue property) or
TJsControlProperty(ValueNode property)

class UI5ControlProperty extends TUI5ControlProperty {
XmlAttribute asXmlControlProperty() { this = TXmlControlProperty(result) }

JsonValue asJsonControlProperty() { this = TJsonControlProperty(result) }

ValueNode asJsControlProperty() { this = TJsControlProperty(result) }

string toString() {
result = this.asXmlControlProperty().getValue().toString() or
result = this.asJsonControlProperty().toString() or
result = this.asJsControlProperty().toString()
}

UI5Control getControl() {
result.asXmlControl() = this.asXmlControlProperty().getElement() or
result.asJsonControl() = this.asJsonControlProperty().getParent() or
result.asJsControl().getArgument(0).asExpr() = this.asJsControlProperty().getEnclosingExpr()
}

string getName() {
result = this.asXmlControlProperty().getName()
or
exists(JsonValue parent | parent.getPropValue(result) = this.asJsonControlProperty())
or
exists(Property property |
property.getAChildExpr() = this.asJsControlProperty().asExpr() and result = property.getName()
)
}

string getValue() {
result = this.asXmlControlProperty().getValue() or
result = this.asJsonControlProperty().getStringValue() or
result = this.asJsControlProperty().asExpr().(StringLiteral).getValue()
}
}
Loading
Loading