Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
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