From b84195a0b25ad181e2a8e94e111525eabfbc26b2 Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Tue, 30 Sep 2025 13:13:21 -0400 Subject: [PATCH 01/17] Support instantiated controls via `NewNode` A TODO is to model `placeAt` and the DOM hierarchy between instantiated controls. --- javascript/frameworks/ui5/ext/ui5.model.yml | 48 +++---- .../frameworks/ui5/RemoteFlowSources.qll | 60 +++++++-- .../javascript/frameworks/ui5/UI5.qll | 124 +++++++++++------- .../javascript/frameworks/ui5/UI5XssQuery.qll | 51 ++++++- .../frameworks/ui5/dataflow/DataFlow.qll | 16 ++- .../javascript_sap_ui5_all/Customizations.qll | 12 +- 6 files changed, 218 insertions(+), 93 deletions(-) diff --git a/javascript/frameworks/ui5/ext/ui5.model.yml b/javascript/frameworks/ui5/ext/ui5.model.yml index 62bd90241..2905e6a66 100644 --- a/javascript/frameworks/ui5/ext/ui5.model.yml +++ b/javascript/frameworks/ui5/ext/ui5.model.yml @@ -15,9 +15,9 @@ extensions: - ["RenderManager", "Renderer", "Member[render].Parameter[0]"] - ["Patcher", "Patcher", "Instance"] - ["Patcher", "sap/ui/core/Patcher", ""] - - ["HTMLControl", "HTMLControl", "Instance"] - - ["HTMLControl", "sap/ui/core/HTML", ""] - - ["HTMLControl", "global", "Member[sap].Member[ui].Member[core].Member[HTML]"] + - ["UI5HTMLControl", "UI5HTMLControl", "Instance"] + - ["UI5HTMLControl", "sap/ui/core/HTML", ""] + - ["UI5HTMLControl", "global", "Member[sap].Member[ui].Member[core].Member[HTML]"] - ["Assert", "global", "Member[sap].Member[base].Member[assert]"] - ["Assert", "global", "Member[jQuery].Member[sap].Member[assert]"] - ["SapLogger", "sap/base/Log", ""] @@ -41,21 +41,21 @@ extensions: - ["SapUIDom", "sap/ui/dom", ""] - ["SapUIDom", "global", "Member[sap].Member[ui].Member[dom]"] # model Controls inheritance - - ["UI5Control", "UI5Control", "Instance"] - - ["UI5Control", "sap/m/InputBase", ""] - - ["UI5Control", "sap/m/Input", ""] - - ["UI5Control", "sap/ui/webc/main/Input", ""] - - ["UI5Control", "sap/ui/commons/TextField", ""] - - ["UI5Control", "sap/m/ComboBoxTextField", ""] - - ["UI5Control", "sap/m/MaskEnabler", ""] - - ["UI5Control", "sap/m/MaskInput", ""] - - ["UI5Control", "sap/m/TextArea", ""] - - ["UI5Control", "sap/m/ComboBoxBase", ""] - - ["UI5Control", "sap/m/MultiInput", ""] - - ["UI5Control", "sap/ui/webc/main/MultiInput", ""] - - ["UI5Control", "sap/m/SearchField", ""] - - ["UI5Control", "sap/m/FeedInput", ""] - - ["UI5Control", "sap/ui/richtexteditor/RichTextEditor", ""] + - ["UI5InputControl", "UI5InputControl", "Instance"] + - ["UI5InputControl", "sap/m/InputBase", ""] + - ["UI5InputControl", "sap/m/Input", ""] + - ["UI5InputControl", "sap/ui/webc/main/Input", ""] + - ["UI5InputControl", "sap/ui/commons/TextField", ""] + - ["UI5InputControl", "sap/m/ComboBoxTextField", ""] + - ["UI5InputControl", "sap/m/MaskEnabler", ""] + - ["UI5InputControl", "sap/m/MaskInput", ""] + - ["UI5InputControl", "sap/m/TextArea", ""] + - ["UI5InputControl", "sap/m/ComboBoxBase", ""] + - ["UI5InputControl", "sap/m/MultiInput", ""] + - ["UI5InputControl", "sap/ui/webc/main/MultiInput", ""] + - ["UI5InputControl", "sap/m/SearchField", ""] + - ["UI5InputControl", "sap/m/FeedInput", ""] + - ["UI5InputControl", "sap/ui/richtexteditor/RichTextEditor", ""] - ["UI5CodeEditor", "UI5CodeEditor", "Instance"] - ["UI5CodeEditor", "sap/ui/codeeditor/CodeEditor", ""] # Classes that provide Path injection APIs @@ -69,8 +69,8 @@ extensions: pack: codeql/javascript-all extensible: "sourceModel" data: - - ["UI5Control", "Member[value]", "remote"] - - ["UI5Control", "Member[getValue].ReturnValue", "remote"] + - ["UI5InputControl", "Member[value]", "remote"] + - ["UI5InputControl", "Member[getValue].ReturnValue", "remote"] - ["UI5CodeEditor", "Member[value]", "remote"] - ["UI5CodeEditor", "Member[getCurrentValue].ReturnValue", "remote"] - ["global", "Member[jQuery].Member[sap].Member[syncHead,syncGet,syncGetText,syncPost,syncPostText].ReturnValue", "remote"] @@ -83,9 +83,9 @@ extensions: data: # html-injection - ["global", "Member[jQuery].Member[sap].Member[globalEval].Argument[0]", "ui5-html-injection"] - - ["HTMLControl", "Argument[0].Member[content]", "ui5-html-injection"] - - ["HTMLControl", "Member[content]", "ui5-html-injection"] - - ["HTMLControl", "Member[setContent].Argument[0]", "ui5-html-injection"] + - ["UI5HTMLControl", "Argument[0].Member[content]", "ui5-html-injection"] + - ["UI5HTMLControl", "Member[content]", "ui5-html-injection"] + - ["UI5HTMLControl", "Member[setContent].Argument[0]", "ui5-html-injection"] - ["Patcher", "Member[unsafeHtml].Argument[0..]", "ui5-html-injection"] - ["RenderManager", "Member[write,unsafeHtml].Argument[0..]", "ui5-html-injection"] - ["RenderManager", "Member[writeAttribute,addStyle].Argument[0..1]", "ui5-html-injection"] @@ -131,3 +131,5 @@ extensions: - ["sap/base/security/encodeURL", "", "Argument[0]", "ReturnValue", "taint"] - ["sap/base/security/encodeURLParameters", "", "Argument[0]", "ReturnValue", "taint"] - ["sap/base/security/encodeXML", "", "Argument[0]", "ReturnValue", "taint"] + - ["UI5HTMLControl", "", "Argument[0].Member[content]", "ReturnValue.Member[content]", "taint"] + - ["UI5HTMLControl", "Instance.Member[getContent]", "Argument[this].Member[content]", "ReturnValue", "taint"] \ No newline at end of file diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll index 858697bfa..e71be10da 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll @@ -1,22 +1,52 @@ import javascript import advanced_security.javascript.frameworks.ui5.UI5 import advanced_security.javascript.frameworks.ui5.UI5View -private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions +import semmle.javascript.security.dataflow.XssThroughDomCustomizations +private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions -private class DataFromRemoteControlReference extends RemoteFlowSource, MethodCallNode { +private class DataFromRemoteControlReference extends RemoteFlowSource { DataFromRemoteControlReference() { exists(UI5Control sourceControl, string typeAlias, ControlReference controlReference | - ApiGraphModelsExtensions::typeModel(typeAlias, sourceControl.getImportPath(), _) and - ApiGraphModelsExtensions::sourceModel(typeAlias, _, "remote", _) and + typeModel(typeAlias, sourceControl.getImportPath(), _) and + sourceModel(typeAlias, _, "remote", _) and sourceControl.getAReference() = controlReference and - controlReference.flowsTo(this.getReceiver()) and - this.getMethodName() = "getValue" + ( + this = controlReference.getAMemberCall("getValue") or + this = controlReference.getAPropertyRead("value") + ) ) } override string getSourceType() { result = "Data from a remote control" } } +private class InputControlInstantiation extends ElementInstantiation { + InputControlInstantiation() { typeModel("UI5InputControl", this.getImportPath(), _) } +} + +private class DataFromInstantiatedAndPlacedAtControl extends RemoteFlowSource, XssThroughDom::Source +{ + DataFromInstantiatedAndPlacedAtControl() { + exists( + InputControlInstantiation controlInstantiation, string typeAlias, + ControlReference controlReference + | + /* Double check that the type derives a remote flow source. */ + typeModel(typeAlias, controlInstantiation.getImportPath(), _) and + sourceModel(typeAlias, _, "remote", _) and + controlInstantiation.getId() = controlReference.getId() and + ( + this = controlReference.getAMemberCall("getValue") or + this = controlReference.getAPropertyRead("value") + ) + ) + } + + override string getSourceType() { + result = "Data from an instantiated control placed in a DOM tree" + } +} + class LocalModelContentBoundBidirectionallyToSourceControl extends RemoteFlowSource { UI5BindingPath bindingPath; UI5Control controlDeclaration; @@ -60,24 +90,28 @@ class ODataServiceModel extends UI5ExternalModel { ODataServiceModel() { exists(MethodCallNode setModelCall, CustomController controller | /* - * 1. This flows from a DF node corresponding to the parent component's model to the `this.setModel` call - * i.e. Aims to capture something like `this.getOwnerComponent().getModel("someModelName")` as in - * `this.getView().setModel(this.getOwnerComponent().getModel("someModelName"))` + * 1. This flows from a DF node corresponding to the parent component's model + * to the `this.setModel` call. e.g. + * + * `this.getOwnerComponent().getModel("someModelName")` as in + * `this.getView().setModel(this.getOwnerComponent().getModel("someModelName"))`. */ modelName = this.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() and this.getCalleeName() = "getModel" and controller.getOwnerComponentRef().flowsTo(this.(MethodCallNode).getReceiver()) and this.flowsTo(setModelCall.getArgument(0)) and - setModelCall.getMethodName() = "setModel" and - setModelCall.getReceiver() = controller.getAViewReference() and - /* 2. The component's manifest.json declares the DataSource as being of OData type */ + setModelCall = controller.getAViewReference().getAMemberCall("setModel") and + /* + * 2. The component's `manifest.json` declares the DataSource as being of OData type. + */ + controller.getOwnerComponent().getExternalModelDef(modelName).getDataSource() instanceof ODataDataSourceManifest ) or /* - * A constructor call to sap.ui.model.odata.v2.ODataModel. + * A constructor call to `sap.ui.model.odata.v2.ODataModel`. */ this instanceof NewNode and diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll index be274c2e7..4660b2f39 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll @@ -5,6 +5,7 @@ import semmle.javascript.security.dataflow.DomBasedXssCustomizations import advanced_security.javascript.frameworks.ui5.UI5View import advanced_security.javascript.frameworks.ui5.UI5HTML import codeql.util.FileSystem +private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions private module WebAppResourceRootJsonReader implements JsonParser::MakeJsonReaderSig { class JsonReader extends WebApp { @@ -133,10 +134,11 @@ class WebApp extends HTML::HtmlFile { } /** - * https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.loader%23methods/sap.ui.loader.config + * The global instance of `sap.ui.Core` that represents this UI5 application, as retrieved + * by a call to `sap.ui.getCore()`. */ -class Loader extends CallNode { - Loader() { this = globalVarRef("sap").getAPropertyRead("ui").getAMethodCall("loader") } +class SapUiCore extends MethodCallNode { + SapUiCore() { this = globalVarRef("sap").getAPropertyRead("ui").getAMethodCall("getCore") } } /** @@ -291,24 +293,31 @@ class CustomControl extends SapExtendCall { result = renderer.getRenderer() ) } + + ValueNode getAThisNode() { + exists(ThisNode thisNode | thisNode.getBinder() = this.getAMethod() | + result.getALocalSource() = thisNode + ) + } } abstract class Reference extends MethodCallNode { } /** - * A JS reference to a `UI5Control`, commonly obtained via `View.byId(controlId)`. + * A JS reference to a `UI5Control`, commonly obtained via its ID. */ class ControlReference extends Reference { string controlId; ControlReference() { - exists(CustomController controller | - ( - controller.getAViewReference().flowsTo(this.getReceiver()) or - controller.getAThisNode() = this.getReceiver() - ) and - this.getMethodName() = "byId" and - this.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() = controlId + this.getArgument(0).getStringValue() = controlId and + ( + exists(CustomController controller | + this = controller.getAViewReference().getAMemberCall("byId") or + this.getReceiver() = controller.getAThisNode() + ) + or + exists(SapUiCore sapUiCore | this = sapUiCore.getAMemberCall("byId")) ) } @@ -453,13 +462,7 @@ class CustomController extends SapExtendCall { UI5View getView() { this = result.getController() } ControlReference getAControlReference() { - exists(MethodCallNode viewRef | - viewRef = this.getAViewReference() and - /* There is a view */ - viewRef.flowsTo(result.(MethodCallNode).getReceiver()) and - /* The result is a member of this view */ - result.(MethodCallNode).getMethodName() = "byId" - ) + result = this.getAViewReference().getAMemberCall("byId") } ValueNode getAThisNode() { @@ -481,19 +484,16 @@ class CustomController extends SapExtendCall { } UI5Model getModel() { - exists(MethodCallNode setModelCall | - this.getAViewReference().flowsTo(setModelCall.getReceiver()) and - setModelCall.getMethodName() = "setModel" and - result.flowsTo(setModelCall.getAnArgument()) - ) + result = this.getAViewReference().getAMemberCall("setModel").getAnArgument().getALocalSource() } ModelReference getModelReference(string modelName) { - this.getAViewReference().flowsTo(result.getReceiver()) and - result.getModelName() = modelName + result = this.getAViewReference().getAMemberCall(modelName) } - ModelReference getAModelReference() { this.getAViewReference().flowsTo(result.getReceiver()) } + ModelReference getAModelReference() { + result = this.getAViewReference().getAMemberCall("getModel") + } RouterReference getARouterReference() { result.getMethodName() = "getRouter" and @@ -514,9 +514,10 @@ class RouteReference extends MethodCallNode { string name; RouteReference() { - this.getMethodName() = "getRoute" and - this.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() = name and - exists(RouterReference routerReference | routerReference.flowsTo(this.getReceiver())) + exists(RouterReference routerReference | + this = routerReference.getAMemberCall("getRoute") and + this.getArgument(0).getStringValue() = name + ) } string getName() { result = name } @@ -1238,21 +1239,18 @@ class RequiredObject extends Expr { */ class SapExtendCall extends InvokeNode, MethodCallNode { SapExtendCall() { - /* 1. The receiver object is an imported one */ exists(RequiredObject requiredModule | - requiredModule.asSourceNode().flowsTo(this.getReceiver()) - ) and - /* 2. The method name is `extend` */ - this.(MethodCallNode).getMethodName() = "extend" + this = requiredModule.asSourceNode().getAMemberCall("extend") + ) } FunctionNode getMethod(string methodName) { - result = this.getContent().(ObjectLiteralNode).getAPropertySource(methodName).(FunctionNode) + result = this.getContent().(ObjectLiteralNode).getAPropertySource(methodName) } FunctionNode getAMethod() { result = this.getMethod(_) } - string getName() { result = this.getArgument(0).asExpr().(StringLiteral).getValue() } + string getName() { result = this.getArgument(0).getStringValue() } ObjectLiteralNode getContent() { result = this.getArgument(1) } @@ -1269,14 +1267,34 @@ class SapExtendCall extends InvokeNode, MethodCallNode { SapDefineModule getDefine() { this.getEnclosingFunction() = result.getArgument(1) } } +class ElementInstantiation extends NewNode { + string importPath; + + ElementInstantiation() { + exists(RequiredObject requiredObject | + this = requiredObject.asSourceNode().getAnInstantiation() and + importPath = requiredObject.getDependency() + ) + } + + string getId() { + result = this.getArgument(0).(SourceNode).getAPropertyWrite("id").getRhs().getStringValue() + } + + string getImportPath() { result = importPath } +} + private newtype TSapElement = - DefinitionOfElement(SapExtendCall extension) or - ReferenceOfElement(Reference reference) + TDefinitionOfElement(SapExtendCall extension) or + TReferenceOfElement(Reference reference) or + TInstantiationOfElement(ElementInstantiation newNode) class SapElement extends TSapElement { - SapExtendCall asDefinition() { this = DefinitionOfElement(result) } + SapExtendCall asDefinition() { this = TDefinitionOfElement(result) } - Reference asReference() { this = ReferenceOfElement(result) } + Reference asReference() { this = TReferenceOfElement(result) } + + ElementInstantiation asInstantiation() { this = TInstantiationOfElement(result) } SapElement getParentElement() { result.asReference() = this.asDefinition().(CustomControl).getController().getAViewReference() or @@ -1286,8 +1304,27 @@ class SapElement extends TSapElement { result.asDefinition() = this.asDefinition().(CustomController).getOwnerComponent() or result.asDefinition() = this.asReference().(ControllerReference).getDefinition().getOwnerComponent() + /* TODO: Implement branch for `asInstantiation` */ } + string getId() { + result = this.asInstantiation().getId() + or + /* TODO: Needs testing */ + result = + this.asDefinition() + .(CustomControl) + .getMetadata() + .getProperty("id") + .getAPropertySource() + .getStringValue() + /* + * Note that because we cannot statically determine the ID of an element from the references alone, + * we do not implement the branch of `TReferenceOfElement`. + */ + + } + string toString() { result = this.asDefinition().toString() or result = this.asReference().toString() @@ -1312,11 +1349,8 @@ class Metadata extends ObjectLiteralNode { Metadata() { this = extension.getContent().getAPropertySource("metadata") } - SourceNode getProperty(string name) { - result = - any(PropertyMetadata property | - property.getParentMetadata() = this and property.getName() = name - ) + PropertyMetadata getProperty(string name) { + result.getParentMetadata() = this and result.getName() = name } } diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5XssQuery.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5XssQuery.qll index 71d83278a..74492e69a 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5XssQuery.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5XssQuery.qll @@ -1,6 +1,7 @@ import javascript import advanced_security.javascript.frameworks.ui5.dataflow.DataFlow as UI5DataFlow import advanced_security.javascript.frameworks.ui5.UI5View +private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions private import semmle.javascript.security.dataflow.DomBasedXssQuery as DomBasedXss module UI5Xss implements DataFlow::ConfigSig { @@ -34,7 +35,8 @@ module UI5Xss implements DataFlow::ConfigSig { predicate isSink(DataFlow::Node node) { node instanceof UI5ExtHtmlISink or - node instanceof UI5ModelHtmlISink + node instanceof UI5ModelHtmlISink or + node instanceof DangerouslySetElementValueOfInstantiatedHTMLControlPlacedAtDom } predicate isAdditionalFlowStep(DataFlow::Node start, DataFlow::Node end) { @@ -67,5 +69,50 @@ class UI5ModelHtmlISink extends DataFlow::Node { * An HTML injection sink typically for custom controls whose RenderManager calls acting as sinks. */ private class UI5ExtHtmlISink extends DataFlow::Node { - UI5ExtHtmlISink() { this = ModelOutput::getASinkNode("ui5-html-injection").asSink() } + UI5ExtHtmlISink() { + this = ModelOutput::getASinkNode("ui5-html-injection").asSink() and + not this.asExpr().getParent+() instanceof NewExpr + } +} + +private class HTMLControlInstantiation extends ElementInstantiation { + HTMLControlInstantiation() { typeModel("UI5HTMLControl", this.getImportPath(), _) } +} + +/** + * The DOM value of a UI5 control that is dynamically generated then placed at + * a certain position in a DOM. + */ +/* TODO: Needs testing of each branch */ +class DangerouslySetElementValueOfInstantiatedHTMLControlPlacedAtDom extends DataFlow::Node { + DangerouslySetElementValueOfInstantiatedHTMLControlPlacedAtDom() { + /* 1. The content is set via the first argument of the constructor. */ + exists(HTMLControlInstantiation htmlControlInstantiation | + exists(string typeAlias | + typeModel(typeAlias, htmlControlInstantiation.getImportPath(), _) and + /* Double check that the type derives a UI5-XSS sink. */ + sinkModel(typeAlias, _, "ui5-html-injection", _) and + exists(PropWrite propWrite | + this = propWrite.getRhs() and + propWrite.getBase() = htmlControlInstantiation.getArgument(0) and + propWrite.getPropertyName() = "content" + ) + ) + or + /* 2. The content is written to the reference of the control. */ + exists(ControlReference controlReference | + htmlControlInstantiation.getId() = controlReference.getId() + | + /* + * 2-1. The content is written directly to the `content` property of the control + * reference. + */ + + this = controlReference.getAPropertyWrite("content") + or + /* 2-2. The content is written using the `setContent` setter. */ + this = controlReference.getAMemberCall("content").getArgument(0) + ) + ) + } } diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll index 14cd8947a..410524744 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll @@ -7,9 +7,11 @@ import advanced_security.javascript.frameworks.ui5.dataflow.FlowSteps private import PatchDataFlow /** - * A statically visible part of a local model's content that has a binding path referring to it in a control declaration acting as an HTML injection sink. + * A statically visible part of a local model's content that has a binding path referring to it in a + * control declaration acting as an HTML injection sink. * - * e.g.1. Given a JSON model `oModel` declared in a controller handler and an HTML injection sink `SomeSinkControl` as: + * e.g.1. Given a JSON model `oModel` declared in a controller handler and an HTML injection sink + * `SomeSinkControl` as: * ```javascript * let oModel = new JSONModel({ y: null }); * ``` @@ -19,7 +21,8 @@ private import PatchDataFlow * ``` * The content `y: null` of `oModel` is recognized as an instance of this class. * - * e.g.2. Given a JSON model `oModel` which gains its content from a JSON file and an HTML injection sink `SomeSinkControl` as: + * e.g.2. Given a JSON model `oModel` which gains its content from a JSON file and an HTML injection + * sink `SomeSinkControl` as: * ```javascript * let oModel = new JSONModel("controller/contents.json"); * ``` @@ -155,8 +158,11 @@ module UI5PathGraph Con } /** - * An edge from a content of an internal model to the corresponding binding path in a view, which makes it an edge in the opposite direction as of `bindingPathToInternalModelContent` above. - * In order to ensure that the edge indeed holds, we check if the model's binding mode is declared as two-way. + * An edge from a content of an internal model to the corresponding binding path in a view, + * which makes it an edge in the opposite direction as of `bindingPathToInternalModelContent` + * above. + * In order to ensure that the edge indeed holds, we check if the model's binding mode is + * declared as two-way. * * c.f. `FlowSteps::InternalModelContentToCustomMetadataPropertyStep`. */ diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript_sap_ui5_all/Customizations.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript_sap_ui5_all/Customizations.qll index e6f989590..2df155696 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript_sap_ui5_all/Customizations.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript_sap_ui5_all/Customizations.qll @@ -1,6 +1,8 @@ -// This file is included for use in custom CodeQL bundles (https://github.com/advanced-security/codeql-bundle). -// The contents of this file will be included in the standard library `Customizations.qll`, and will therefore -// be included in the out-of-the-box security queries. -// -// We import under alias to avoid any potential naming conflicts +/* + * This file is included for use in custom CodeQL bundles (https://github.com/advanced-security/codeql-bundle). + * The contents of this file will be included in the standard library `Customizations.qll`, and will therefore + * be included in the out-of-the-box security queries. + */ + +/* We import under alias to avoid any potential naming conflicts */ import advanced_security.javascript.frameworks.ui5.RemoteFlowSources as UI5RemoteFlowSources From 78ec64387f4d38e009cbf9b66a61edabd1bb1651 Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Wed, 1 Oct 2025 11:09:51 -0400 Subject: [PATCH 02/17] Replace all `E.getReceiver().getALocalSource()` with `.getAMemberCall()` The latter is a bad idea as it misses some local data flow steps. --- .../javascript/frameworks/ui5/UI5.qll | 168 +++++++++--------- 1 file changed, 81 insertions(+), 87 deletions(-) diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll index 4660b2f39..53890acc0 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll @@ -214,9 +214,13 @@ class JQuerySap extends DataFlow::SourceNode { * A user-defined module through `jQuery.sap.declare`. */ class JQueryDefineModule extends UserModule, MethodCallExpr { - JQueryDefineModule() { exists(JQuerySap jquerySap | jquerySap.asExpr() = this.getReceiver()) } + JQueryDefineModule() { + exists(JQuerySap jQuerySap | this = jQuerySap.getAMemberCall(["declare", "define"]).asExpr()) + } - override string getADependency() { result = this.getArgument(0).getStringValue() } + override string getADependency() { + result = this.getArgument(0).getALocalSource().getStringValue() + } override string getModuleFileRelativePath() { result = this.getFile().getRelativePath() } @@ -229,8 +233,9 @@ class JQueryDefineModule extends UserModule, MethodCallExpr { class Renderer extends SapExtendCall { Renderer() { - this.getReceiver().getALocalSource() = + this = TypeTrackers::hasDependency(["sap/ui/core/Renderer", "sap.ui.core.Renderer"]) + .getAMemberCall("extend") } FunctionNode getRenderer() { @@ -244,8 +249,9 @@ class Renderer extends SapExtendCall { class CustomControl extends SapExtendCall { CustomControl() { - this.getReceiver().getALocalSource() = - TypeTrackers::hasDependency(["sap/ui/core/Control", "sap.ui.core.Control"]) or + this = + TypeTrackers::hasDependency(["sap/ui/core/Control", "sap.ui.core.Control"]) + .getAMemberCall("extend") or exists(SapDefineModule sapModule | this.getDefine() = sapModule.getExtendingModule()) } @@ -293,16 +299,31 @@ class CustomControl extends SapExtendCall { result = renderer.getRenderer() ) } +} - ValueNode getAThisNode() { - exists(ThisNode thisNode | thisNode.getBinder() = this.getAMethod() | - result.getALocalSource() = thisNode +class ControlPlaceAtCall extends MethodCallNode { + ControlPlaceAtCall() { + exists(SapElement ui5Control | + // /* 1. `this.placeAt(...)` in a custom control definition */ + // this = ui5Control.asDefinition().getAThisNode() + // or + // /* + // * 2. `new SomeControl(...).placeAt(...)` where `SomeControl` + // * may be UI5 library control or a custom control + // */ + // ui5Control.asInstantiation(). + // or + // /* 3. `.byId(...).placeAt(...)` */ + // ui5Control.asReference(). + none() // TODO ) } } abstract class Reference extends MethodCallNode { } +private predicate byId(MethodCallNode byId) { byId.getMethodName() = "byId" } + /** * A JS reference to a `UI5Control`, commonly obtained via its ID. */ @@ -310,11 +331,11 @@ class ControlReference extends Reference { string controlId; ControlReference() { - this.getArgument(0).getStringValue() = controlId and + this.getArgument(0).getALocalSource().getStringValue() = controlId and ( exists(CustomController controller | this = controller.getAViewReference().getAMemberCall("byId") or - this.getReceiver() = controller.getAThisNode() + this = controller.getAThisNode().getAMemberCall("byId") ) or exists(SapUiCore sapUiCore | this = sapUiCore.getAMemberCall("byId")) @@ -398,17 +419,11 @@ class ControlReference extends Reference { class ViewReference extends Reference { CustomController controller; - ViewReference() { - this.getMethodName() = "getView" and - controller.getAThisNode() = this.getReceiver() - } + ViewReference() { this = controller.getAThisNode().getAMemberCall("getView") } UI5View getDefinition() { result = controller.getView() } - MethodCallNode getABindElementCall() { - result.getMethodName() = "bindElement" and - this.flowsTo(result.getReceiver()) - } + MethodCallNode getABindElementCall() { result = this.getAMemberCall("bindElement") } } /** @@ -417,7 +432,7 @@ class ViewReference extends Reference { class ControllerReference extends Reference { ViewReference viewReference; - ControllerReference() { viewReference.flowsTo(this.getReceiver()) } + ControllerReference() { this = viewReference.getAMemberCall("getController") } CustomController getDefinition() { result = viewReference.getDefinition().getController() } } @@ -426,8 +441,9 @@ class CustomController extends SapExtendCall { string name; CustomController() { - this.getReceiver().getALocalSource() = - TypeTrackers::hasDependency(["sap/ui/core/mvc/Controller", "sap.ui.core.mvc.Controller"]) and + this = + TypeTrackers::hasDependency(["sap/ui/core/mvc/Controller", "sap.ui.core.mvc.Controller"]) + .getAMemberCall("extend") and name = this.getFile().getBaseName().regexpCapture("([a-zA-Z0-9]+).[cC]ontroller.js", 1) } @@ -447,17 +463,13 @@ class CustomController extends SapExtendCall { } MethodCallNode getOwnerComponentRef() { - this.getAThisNode() = result.getReceiver() and - result.getMethodName() = "getOwnerComponent" + result = this.getAThisNode().getAMemberCall("getOwnerComponent") } /** * Gets a reference to a view object that can be accessed from one of the methods of this controller. */ - ViewReference getAViewReference() { - result.getMethodName() = "getView" and - result.(MethodCallNode).getReceiver() = this.getAThisNode() - } + ViewReference getAViewReference() { result = this.getAThisNode().getAMemberCall("getView") } UI5View getView() { this = result.getController() } @@ -465,21 +477,19 @@ class CustomController extends SapExtendCall { result = this.getAViewReference().getAMemberCall("byId") } - ValueNode getAThisNode() { - exists(ThisNode thisNode | thisNode.getBinder() = this.getAMethod() | - /* ========== 1. `this` referring to the binder ========== */ - thisNode.flowsTo(result) - or - /* 2. ========== `this` bound to an outside `this` ========== */ - /* - * 2-1. The DisplayEventHandler's `this` bound to an outside `this` via - * `.attachDisplay` or `.detachDisplay` - */ + override ThisNode getAThisNode() { + /* 1. `this` referring to the binder */ + result = super.getAThisNode() + or + /* 2. `this` bound to a callback's `this` */ + /* + * 2-1. The this node of `.attachDisplay` or `.detachDisplay` also represents this + * controller. + */ - exists(DisplayEventHandler handler, ThisNode handlerThis | handlerThis.getBinder() = handler | - thisNode.flowsTo(handler.getAssociatedContextObject()) and - handlerThis.flowsTo(result) - ) + exists(DisplayEventHandler handler | + handler.getAssociatedContextObject().getALocalSource() = this.getAThisNode() and + result.getBinder() = handler ) } @@ -516,7 +526,7 @@ class RouteReference extends MethodCallNode { RouteReference() { exists(RouterReference routerReference | this = routerReference.getAMemberCall("getRoute") and - this.getArgument(0).getStringValue() = name + this.getArgument(0).getALocalSource().getStringValue() = name ) } @@ -545,10 +555,9 @@ class ControllerHandler extends EventHandler { class RouterReference extends MethodCallNode { RouterReference() { - this.getMethodName() = "getRouter" and exists(CustomController controller | - controller.getAThisNode() = this.getReceiver() or - controller.getOwnerComponentRef().flowsTo(this.getReceiver()) + this = controller.getAThisNode().getAMemberCall("getRouter") or + this = controller.getOwnerComponentRef().getAMemberCall("getRouter") ) } @@ -565,9 +574,8 @@ class RoutingTarget extends MethodCallNode { RouterReference routerReference; RoutingTarget() { - this.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() = name and - routerReference = this.getReceiver().getALocalSource() and - this.getMethodName() = "getTarget" + this.getArgument(0).getALocalSource().getStringValue() = name and + this = routerReference.getAMemberCall("getTarget") } RouterReference getRouterReference() { result = routerReference } @@ -615,17 +623,14 @@ class DisplayEventHandler extends EventHandler { */ class ModelReference extends MethodCallNode { ModelReference() { - this.getMethodName() = "getModel" and - ( - exists(ViewReference view | view.flowsTo(this.getReceiver())) - or - exists(CustomController controller | - controller.getAViewReference().flowsTo(this.getReceiver()) or - controller.getOwnerComponentRef().flowsTo(this.getReceiver()) - ) - or - exists(Component component | component.getAThisNode().flowsTo(this.getReceiver())) + exists(ViewReference view | this = view.getAMemberCall("getModel")) + or + exists(CustomController controller | + this = controller.getAViewReference().getAMemberCall("getModel") or + this = controller.getOwnerComponentRef().getAMemberCall("getModel") ) + or + exists(Component component | this = component.getAThisNode().getAMemberCall("getModel")) } predicate isDefaultModelReference() { this.getNumArgument() = 0 } @@ -633,9 +638,7 @@ class ModelReference extends MethodCallNode { /** * Gets the models' name being referred to, given that it can be statically determined. */ - string getModelName() { - result = this.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() - } + string getModelName() { result = this.getArgument(0).getALocalSource().getStringValue() } predicate isLocalModelReference() { exists(InternalModelManifest internalModelManifest | @@ -704,10 +707,7 @@ class ModelReference extends MethodCallNode { /** * Gets a `getProperty` or `getObject` method call on this `ModelReference`. These methods read from a single property of the model this refers to. */ - MethodCallNode getARead() { - result.getMethodName() = ["getProperty", "getObject"] and - result.getReceiver().getALocalSource() = this - } + MethodCallNode getARead() { result = this.getAMemberCall(["getProperty", "getObject"]) } /** * Gets the resolved model of this `ModelReference` by looking for a matching `setModel` call. @@ -724,10 +724,7 @@ abstract class UI5Model extends InvokeNode { /** * A `getProperty` or `getObject` method call on this `UI5Model`. These methods read from a single property of this model. */ - MethodCallNode getARead() { - result.getMethodName() = ["getProperty", "getObject"] and - result.getReceiver().getALocalSource() = this - } + MethodCallNode getARead() { result = this.getAMemberCall(["getProperty", "getObject"]) } } /** @@ -748,16 +745,16 @@ import ManifestJson */ class Component extends SapExtendCall { Component() { - this.getReceiver().getALocalSource() = - /* - * Represents models that are loaded from an external source, e.g. OData service. - * It is the value flowing to a `setModel` call in a handler of a `CustomController` (which is represented by `ControllerHandler`), since it is the closest we can get to the actual model itself. - */ + /* + * Represents models that are loaded from an external source, e.g. OData service. + * It is the value flowing to a `setModel` call in a handler of a `CustomController` (which is represented by `ControllerHandler`), since it is the closest we can get to the actual model itself. + */ + this = TypeTrackers::hasDependency([ "sap/ui/core/mvc/Component", "sap.ui.core.mvc.Component", "sap/ui/core/UIComponent", "sap.ui.core.UIComponent" - ]) + ]).getAMemberCall("extend") } string getId() { result = this.getName().regexpCapture("([a-zA-Z0-9.]+).Component", 1) } @@ -788,8 +785,6 @@ class Component extends SapExtendCall { } ExternalModelManifest getAnExternalModelDef() { result = this.getExternalModelDef(_) } - - ThisNode getAThisNode() { result.getBinder() = this.getAMethod() } } module ManifestJson { @@ -1185,20 +1180,17 @@ class ResourceModel extends UI5Model, ModelReference { /* A model reference obtained from this.getOwnerComponent().getModel("i18n") */ exists(CustomController controller, ResourceModelManifest manifest | ( - controller.getAThisNode() = this.getReceiver() or - controller.getOwnerComponentRef().flowsTo(this.(ModelReference).getReceiver()) + this = controller.getAThisNode().getAMemberCall("getModel") or + this = controller.getOwnerComponentRef().getAMemberCall("getModel") ) and modelName = this.getModelName() and manifest.getName() = modelName ) } - override MethodCallNode getARead() { result = this.(ModelReference).getARead() } + override MethodCallNode getARead() { result = ModelReference.super.getARead() } - MethodCallNode getResourceBundle() { - result.getMethodName() = "getResourceBundle" and - this = result.getReceiver().getALocalSource() - } + MethodCallNode getResourceBundle() { result = this.getAMemberCall("getResourceBundle") } } class BindingMode extends RequiredObject { @@ -1250,7 +1242,7 @@ class SapExtendCall extends InvokeNode, MethodCallNode { FunctionNode getAMethod() { result = this.getMethod(_) } - string getName() { result = this.getArgument(0).getStringValue() } + string getName() { result = this.getArgument(0).getALocalSource().getStringValue() } ObjectLiteralNode getContent() { result = this.getArgument(1) } @@ -1265,6 +1257,8 @@ class SapExtendCall extends InvokeNode, MethodCallNode { /** Gets the `sap.ui.define` call that wraps this extension. */ SapDefineModule getDefine() { this.getEnclosingFunction() = result.getArgument(1) } + + ThisNode getAThisNode() { result.getBinder() = this.getAMethod() } } class ElementInstantiation extends NewNode { @@ -1402,10 +1396,10 @@ class PropertyMetadata extends ObjectLiteralNode { */ predicate isUnrestrictedStringType() { /* text : "string" */ - this.asExpr().(StringLiteral).getValue() = "string" + this.getStringValue() = "string" or /* text: { type: "string" } */ - this.getAPropertySource("type").asExpr().(StringLiteral).getValue() = "string" + this.getAPropertySource("type").getStringValue() = "string" or /* text: { someOther: "someOtherVal", ... } */ not exists(this.getAPropertySource("type")) From 70cc90424c1c504e920501e7e2f91c898b93ffdf Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Wed, 1 Oct 2025 14:04:08 -0400 Subject: [PATCH 03/17] Convert more to using better predicate calls --- .../javascript/frameworks/ui5/UI5.qll | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll index 53890acc0..c0b7447df 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll @@ -218,9 +218,7 @@ class JQueryDefineModule extends UserModule, MethodCallExpr { exists(JQuerySap jQuerySap | this = jQuerySap.getAMemberCall(["declare", "define"]).asExpr()) } - override string getADependency() { - result = this.getArgument(0).getALocalSource().getStringValue() - } + override string getADependency() { result = this.getArgument(0).getStringValue() } override string getModuleFileRelativePath() { result = this.getFile().getRelativePath() } @@ -588,13 +586,11 @@ class RoutingTarget extends MethodCallNode { } MethodCallNode getAnAttachDisplayCall() { - result.getReceiver().getALocalSource() = routerReference.getATarget() and - result.getMethodName() = "attachDisplay" + result = routerReference.getATarget().getAMemberCall("attachDisplay") } MethodCallNode getADetachDisplayCall() { - result.getReceiver().getALocalSource() = routerReference.getATarget() and - result.getMethodName() = "detachDisplay" + result = routerReference.getATarget().getAMemberCall("detachDisplay") } } From 8c08f3dd4944359c1611b29520457be274c3b1ba Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Wed, 1 Oct 2025 14:04:56 -0400 Subject: [PATCH 04/17] Convert more to using better predicate calls --- .../frameworks/ui5/RemoteFlowSources.qll | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll index e71be10da..0411c526a 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll @@ -133,21 +133,17 @@ private class RouteParameterAccess extends RemoteFlowSource instanceof PropRead override string getSourceType() { result = "RouteParameterAccess" } RouteParameterAccess() { - exists( - ControllerHandler handler, RouteManifest routeManifest, ParameterNode handlerParameter, - MethodCallNode getParameterCall - | + exists(ControllerHandler handler, RouteManifest routeManifest, MethodCallNode getParameterCall | handler.isAttachedToRoute(routeManifest.getName()) and this.asExpr().getEnclosingFunction() = handler.getFunction() and - handlerParameter = handler.getParameter(0) and - getParameterCall.getMethodName() = "getParameter" and - getParameterCall.getReceiver().getALocalSource() = handlerParameter and + getParameterCall = handler.getParameter(0).getAMemberCall("getParameter") and ( - routeManifest.matchesPathString(this.getPropertyName()) and - this.getBase().getALocalSource() = getParameterCall + exists(string path | + this = getParameterCall.getAPropertyRead(path) and + routeManifest.matchesPathString(path) + ) or - /* TODO: Why does `routeManifest.matchesPathString` not work for propertyName?? */ - this.getBase().(PropRead).getBase().getALocalSource() = getParameterCall + this = getParameterCall.getAPropertyRead().getAPropertyRead() ) ) } @@ -157,10 +153,8 @@ private class DisplayEventHandlerParameterAccess extends RemoteFlowSource instan override string getSourceType() { result = "DisplayEventHandlerParameterAccess" } DisplayEventHandlerParameterAccess() { - exists(DisplayEventHandler handler, MethodCallNode getParameterCall | - getParameterCall.getMethodName() = "getParameter" and - this.getBase().getALocalSource() = getParameterCall and - handler.getParameter(0) = getParameterCall.getReceiver().getALocalSource() + exists(DisplayEventHandler handler | + this = handler.getParameter(0).getAMemberCall("getParameter").getAPropertyRead() ) } } From ac8ee5020a6c336b1bd60030fbee627101b3c82e Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Wed, 1 Oct 2025 14:08:52 -0400 Subject: [PATCH 05/17] Convert more to using better predicate calls --- .../javascript/frameworks/ui5/UI5.qll | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll index c0b7447df..6292f9606 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll @@ -485,9 +485,8 @@ class CustomController extends SapExtendCall { * controller. */ - exists(DisplayEventHandler handler | - handler.getAssociatedContextObject().getALocalSource() = this.getAThisNode() and - result.getBinder() = handler + exists(DisplayEventHandler handler | handler = result.getBinder() | + handler.getAssociatedContextObject().getALocalSource() = this.getAThisNode() ) } @@ -504,10 +503,8 @@ class CustomController extends SapExtendCall { } RouterReference getARouterReference() { - result.getMethodName() = "getRouter" and - exists(ThisNode controllerThis | - result.(MethodCallNode).getReceiver() = controllerThis.getALocalUse() and - controllerThis.getBinder() = this.getAMethod() + exists(ThisNode controllerThis | controllerThis.getBinder() = this.getAMethod() | + result = controllerThis.getAMemberCall("getRouter") ) } From dec0ab903f8f2b7d0b8af484d13152301a5425e7 Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Wed, 1 Oct 2025 14:14:07 -0400 Subject: [PATCH 06/17] Fix getModelReference/0 and getModelReference/1 --- .../advanced_security/javascript/frameworks/ui5/UI5.qll | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll index 6292f9606..09f25f93e 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll @@ -495,12 +495,11 @@ class CustomController extends SapExtendCall { } ModelReference getModelReference(string modelName) { - result = this.getAViewReference().getAMemberCall(modelName) + result = this.getAViewReference().getAMemberCall("getModel") and + result.getArgument(0).getALocalSource().getStringValue() = modelName } - ModelReference getAModelReference() { - result = this.getAViewReference().getAMemberCall("getModel") - } + ModelReference getAModelReference() { result = this.getModelReference(_) } RouterReference getARouterReference() { exists(ThisNode controllerThis | controllerThis.getBinder() = this.getAMethod() | From 5bb503b71ef1f8e4b35d699e0d197dcae2d4d26c Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Wed, 15 Oct 2025 16:13:48 -0400 Subject: [PATCH 07/17] Enable precise report of instantiated controls with `placeAt` --- .../frameworks/ui5/RemoteFlowSources.qll | 20 ++++-- .../javascript/frameworks/ui5/UI5.qll | 61 ++++++++++------- .../javascript/frameworks/ui5/UI5XssQuery.qll | 20 ++++-- .../frameworks/ui5/dataflow/DataFlow.qll | 65 +++++++++++++++++++ 4 files changed, 130 insertions(+), 36 deletions(-) diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll index 0411c526a..1aa09907d 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll @@ -24,13 +24,15 @@ private class InputControlInstantiation extends ElementInstantiation { InputControlInstantiation() { typeModel("UI5InputControl", this.getImportPath(), _) } } -private class DataFromInstantiatedAndPlacedAtControl extends RemoteFlowSource, XssThroughDom::Source +private module TrackPlaceAtCallConfigFlow = TaintTracking::Global; + +class DataFromInstantiatedAndPlacedAtControl extends RemoteFlowSource, XssThroughDom::Source { + InputControlInstantiation controlInstantiation; + ControlPlaceAtCall placeAtCall; + DataFromInstantiatedAndPlacedAtControl() { - exists( - InputControlInstantiation controlInstantiation, string typeAlias, - ControlReference controlReference - | + exists(string typeAlias, ControlReference controlReference | /* Double check that the type derives a remote flow source. */ typeModel(typeAlias, controlInstantiation.getImportPath(), _) and sourceModel(typeAlias, _, "remote", _) and @@ -39,12 +41,18 @@ private class DataFromInstantiatedAndPlacedAtControl extends RemoteFlowSource, X this = controlReference.getAMemberCall("getValue") or this = controlReference.getAPropertyRead("value") ) - ) + ) and + TrackPlaceAtCallConfigFlow::flow(controlInstantiation, placeAtCall) } override string getSourceType() { result = "Data from an instantiated control placed in a DOM tree" } + + ControlPlaceAtCall getPlaceAtCall() { + // result = "TODO" + none() // TODO + } } class LocalModelContentBoundBidirectionallyToSourceControl extends RemoteFlowSource { diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll index 09f25f93e..3134e3635 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll @@ -302,26 +302,27 @@ class CustomControl extends SapExtendCall { class ControlPlaceAtCall extends MethodCallNode { ControlPlaceAtCall() { exists(SapElement ui5Control | - // /* 1. `this.placeAt(...)` in a custom control definition */ - // this = ui5Control.asDefinition().getAThisNode() - // or - // /* - // * 2. `new SomeControl(...).placeAt(...)` where `SomeControl` - // * may be UI5 library control or a custom control - // */ - // ui5Control.asInstantiation(). - // or - // /* 3. `.byId(...).placeAt(...)` */ - // ui5Control.asReference(). - none() // TODO + /* 1. `this.placeAt(...)` in a custom control definition */ + this = ui5Control.asDefinition().getAThisNode().getAMemberCall("placeAt") + or + /* + * 2. `new SomeControl(...).placeAt(...)` where + * `SomeControl` may be UI5 library control or a custom control + */ + + this = ui5Control.asInstantiation().getAMemberCall("placeAt") + or + // this = ui5Control.getParentElement*().asInstantiation().getAMemberCall("placeAt") or + /* 3. `.byId(...).placeAt(...)` */ + this = ui5Control.asReference().getAMemberCall("placeAt") ) } + + string getDomElementId() { result = this.getArgument(0).getStringValue() } } abstract class Reference extends MethodCallNode { } -private predicate byId(MethodCallNode byId) { byId.getMethodName() = "byId" } - /** * A JS reference to a `UI5Control`, commonly obtained via its ID. */ @@ -476,10 +477,10 @@ class CustomController extends SapExtendCall { } override ThisNode getAThisNode() { - /* 1. `this` referring to the binder */ + /* ========== 1. `this` referring to the binder ========== */ result = super.getAThisNode() or - /* 2. `this` bound to a callback's `this` */ + /* 2. ========== `this` bound to a callback's `this` ========== */ /* * 2-1. The this node of `.attachDisplay` or `.detachDisplay` also represents this * controller. @@ -1289,8 +1290,19 @@ class SapElement extends TSapElement { result.asDefinition() = this.asReference().(ViewReference).getDefinition().getController() or result.asDefinition() = this.asDefinition().(CustomController).getOwnerComponent() or result.asDefinition() = - this.asReference().(ControllerReference).getDefinition().getOwnerComponent() - /* TODO: Implement branch for `asInstantiation` */ + this.asReference().(ControllerReference).getDefinition().getOwnerComponent() or + /* ==================== exists(result.asInstantiation()) branches ==================== */ + result.asInstantiation() = + this.asReference().(ControlReference).getAMemberCall(_).getAnArgument().getALocalSource() or + result.asInstantiation() = + this.asReference().(ControlReference).getAPropertyWrite().getRhs().getALocalSource() + // or + // result.asInstantiation() = + // this.asInstantiation().getAMemberCall(_).getAnArgument().getALocalSource() or + // result.asInstantiation() = this.asInstantiation().getAPropertyWrite().getRhs().getALocalSource() or + // result.asInstantiation() = this.asInstantiation().getAnArgument() + // TrackParentControlConfig::flow(this.asInstantiation()) + /* =================================================================================== */ } string getId() { @@ -1313,15 +1325,14 @@ class SapElement extends TSapElement { string toString() { result = this.asDefinition().toString() or - result = this.asReference().toString() + result = this.asReference().toString() or + result = this.asInstantiation().toString() } - predicate hasLocationInfo( - string filepath, int startline, int startcolumn, int endline, int endcolumn - ) { - this.asDefinition().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) - or - this.asReference().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) + Location getLocation() { + result = this.asDefinition().getLocation() or + result = this.asReference().getLocation() or + result = this.asInstantiation().getLocation() } } diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5XssQuery.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5XssQuery.qll index 74492e69a..2ffea9ff1 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5XssQuery.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5XssQuery.qll @@ -79,15 +79,24 @@ private class HTMLControlInstantiation extends ElementInstantiation { HTMLControlInstantiation() { typeModel("UI5HTMLControl", this.getImportPath(), _) } } +private module TrackPlaceAtCallConfigFlow = TaintTracking::Global; + /** * The DOM value of a UI5 control that is dynamically generated then placed at * a certain position in a DOM. */ -/* TODO: Needs testing of each branch */ -class DangerouslySetElementValueOfInstantiatedHTMLControlPlacedAtDom extends DataFlow::Node { +/* + * TODO 1: Needs testing of each branch + * TODO 2: Model the `placeAt`: + */ + +private class DangerouslySetElementValueOfInstantiatedHTMLControlPlacedAtDom extends DataFlow::Node { + HTMLControlInstantiation htmlControlInstantiation; + ControlPlaceAtCall placeAtCall; + DangerouslySetElementValueOfInstantiatedHTMLControlPlacedAtDom() { - /* 1. The content is set via the first argument of the constructor. */ - exists(HTMLControlInstantiation htmlControlInstantiation | + ( + /* 1. The content is set via the first argument of the constructor. */ exists(string typeAlias | typeModel(typeAlias, htmlControlInstantiation.getImportPath(), _) and /* Double check that the type derives a UI5-XSS sink. */ @@ -113,6 +122,7 @@ class DangerouslySetElementValueOfInstantiatedHTMLControlPlacedAtDom extends Dat /* 2-2. The content is written using the `setContent` setter. */ this = controlReference.getAMemberCall("content").getArgument(0) ) - ) + ) and + TrackPlaceAtCallConfigFlow::flow(htmlControlInstantiation, placeAtCall) } } diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll index 410524744..f6cc00e34 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll @@ -72,6 +72,10 @@ module UI5PathGraph Con UI5BindingPath asUI5BindingPathNode() { this = TUI5BindingPathNode(result) } + predicate isDataFlowNode() { exists(this.asDataFlowNode()) } + + predicate isUI5BindingPathNode() { exists(this.asUI5BindingPathNode()) } + string toString() { result = this.asDataFlowNode().toString() or @@ -218,3 +222,64 @@ module UI5PathGraph Con ui5PathNodeSucc.asUI5BindingPathNode() = any(UI5View view).getAnHtmlISink() } } + +module TrackPlaceAtCallConfig implements DataFlow::ConfigSig { + /** + * A child element instantiation. + */ + predicate isSource(DataFlow::Node node) { node instanceof ElementInstantiation } + + additional predicate isSinkWithPlaceAtCall(DataFlow::Node node, ControlPlaceAtCall placeAtCall) { + node = placeAtCall + } + + /** + * An "extension point" exposed from a parent element instantiation to + * register a child to itself. + */ + predicate isSink(DataFlow::Node node) { isSinkWithPlaceAtCall(node, _) } + + /** + * Step from data being written and the property that is being written to. + * Needed to express the fact that the child control is usually written to + * a property of a parent control to establish the child-parent relationship. + * + * NOTE: The "extension point" exposed by the parent control can come in the + * form of: + * + * 1. `new Parent({ children: [ new Child( ... ) ] })` + * 2. `new Parent(...).children[0] = new Child( ... )` + * 3. `new Parent(...).addChild(new Child( ... ))` + * + * The first and second cases are (nested) `PropWrite`s to `getArgument(0)` + * and are easily covered. But the third case poses a new problem of identifying + * methods that writes to the `children` property, which CodeQL can't verify + * because its definition is in the library. + * + * - [X] Mark all method calls: overgeneralizes, can infer dumb things + * (`sap.m.FlexBox.removeItem` comes to mind), but would still work for all + * true cases + * - [ ] Heuristic by prefix: not an attractive option since heuristics can fail + */ + predicate isAdditionalFlowStep(DataFlow::Node start, DataFlow::Node end) { + exists(DataFlow::PropWrite propWrite | + start = propWrite.getRhs() and + end = propWrite.getBase() + ) + or + exists(DataFlow::MethodCallNode maybeAddingChildAPICall | + start = maybeAddingChildAPICall.getAnArgument() and + end = maybeAddingChildAPICall.getReceiver() + ) + or + exists(DataFlow::MethodCallNode call | + start = call.getReceiver() and + end = call + ) + or + exists(DataFlow::NewNode new | + start = new.getAnArgument() and + end = new + ) + } +} From 4e9cdf0f0568719c2d4805ea78f3725952d0f7b3 Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Wed, 15 Oct 2025 17:24:33 -0400 Subject: [PATCH 08/17] Debutg `JQueryDefineModule` and `CustomController.getAModelReference/0` --- .../javascript/frameworks/ui5/UI5.qll | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll index 3134e3635..8258f9853 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll @@ -215,7 +215,7 @@ class JQuerySap extends DataFlow::SourceNode { */ class JQueryDefineModule extends UserModule, MethodCallExpr { JQueryDefineModule() { - exists(JQuerySap jQuerySap | this = jQuerySap.getAMemberCall(["declare", "define"]).asExpr()) + exists(JQuerySap jQuerySap | this = jQuerySap.getAMemberCall(["declare", "require"]).asExpr()) } override string getADependency() { result = this.getArgument(0).getStringValue() } @@ -496,11 +496,13 @@ class CustomController extends SapExtendCall { } ModelReference getModelReference(string modelName) { - result = this.getAViewReference().getAMemberCall("getModel") and - result.getArgument(0).getALocalSource().getStringValue() = modelName + result = this.getAModelReference() and + result.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() = modelName } - ModelReference getAModelReference() { result = this.getModelReference(_) } + ModelReference getAModelReference() { + result = this.getAViewReference().getAMemberCall("getModel") + } RouterReference getARouterReference() { exists(ThisNode controllerThis | controllerThis.getBinder() = this.getAMethod() | From 4f6bdde61a5899e017ebe77b83c345cdd17225d7 Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Fri, 17 Oct 2025 16:24:58 -0400 Subject: [PATCH 09/17] Clean up code: remove unneeded definitions --- .../frameworks/ui5/RemoteFlowSources.qll | 8 +- .../javascript/frameworks/ui5/UI5.qll | 90 ++++--------------- .../frameworks/ui5/dataflow/DataFlow.qll | 6 +- 3 files changed, 19 insertions(+), 85 deletions(-) diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll index 1aa09907d..66218f248 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll @@ -26,8 +26,7 @@ private class InputControlInstantiation extends ElementInstantiation { private module TrackPlaceAtCallConfigFlow = TaintTracking::Global; -class DataFromInstantiatedAndPlacedAtControl extends RemoteFlowSource, XssThroughDom::Source -{ +class DataFromInstantiatedAndPlacedAtControl extends RemoteFlowSource, XssThroughDom::Source { InputControlInstantiation controlInstantiation; ControlPlaceAtCall placeAtCall; @@ -48,11 +47,6 @@ class DataFromInstantiatedAndPlacedAtControl extends RemoteFlowSource, XssThroug override string getSourceType() { result = "Data from an instantiated control placed in a DOM tree" } - - ControlPlaceAtCall getPlaceAtCall() { - // result = "TODO" - none() // TODO - } } class LocalModelContentBoundBidirectionallyToSourceControl extends RemoteFlowSource { diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll index 8258f9853..9287634d8 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll @@ -301,21 +301,25 @@ class CustomControl extends SapExtendCall { class ControlPlaceAtCall extends MethodCallNode { ControlPlaceAtCall() { - exists(SapElement ui5Control | - /* 1. `this.placeAt(...)` in a custom control definition */ - this = ui5Control.asDefinition().getAThisNode().getAMemberCall("placeAt") - or - /* - * 2. `new SomeControl(...).placeAt(...)` where - * `SomeControl` may be UI5 library control or a custom control - */ + /* 1. `this.placeAt(...)` in a custom control definition. */ + exists(CustomControl control | this = control.getAThisNode().getAMemberCall("placeAt")) + or + /* + * 2. `new SomeControl(...).placeAt(...)` where `SomeControl` may be UI5 + * library control or a custom control. + */ - this = ui5Control.asInstantiation().getAMemberCall("placeAt") - or - // this = ui5Control.getParentElement*().asInstantiation().getAMemberCall("placeAt") or - /* 3. `.byId(...).placeAt(...)` */ - this = ui5Control.asReference().getAMemberCall("placeAt") + exists(ElementInstantiation controlInstantiation | + this = controlInstantiation.getAMemberCall("placeAt") ) + or + /* + * 3. `oController.getView().byId(...).placeAt(...)` where + * `oController.getView().byId(...)` is a reference to a library control + * or a custom control. + */ + + exists(ControlReference controlReference | this = controlReference.getAMemberCall("placeAt")) } string getDomElementId() { result = this.getArgument(0).getStringValue() } @@ -1278,66 +1282,6 @@ private newtype TSapElement = TReferenceOfElement(Reference reference) or TInstantiationOfElement(ElementInstantiation newNode) -class SapElement extends TSapElement { - SapExtendCall asDefinition() { this = TDefinitionOfElement(result) } - - Reference asReference() { this = TReferenceOfElement(result) } - - ElementInstantiation asInstantiation() { this = TInstantiationOfElement(result) } - - SapElement getParentElement() { - result.asReference() = this.asDefinition().(CustomControl).getController().getAViewReference() or - result.asReference() = - this.asReference().(ControlReference).getDefinition().getController().getAViewReference() or - result.asDefinition() = this.asReference().(ViewReference).getDefinition().getController() or - result.asDefinition() = this.asDefinition().(CustomController).getOwnerComponent() or - result.asDefinition() = - this.asReference().(ControllerReference).getDefinition().getOwnerComponent() or - /* ==================== exists(result.asInstantiation()) branches ==================== */ - result.asInstantiation() = - this.asReference().(ControlReference).getAMemberCall(_).getAnArgument().getALocalSource() or - result.asInstantiation() = - this.asReference().(ControlReference).getAPropertyWrite().getRhs().getALocalSource() - // or - // result.asInstantiation() = - // this.asInstantiation().getAMemberCall(_).getAnArgument().getALocalSource() or - // result.asInstantiation() = this.asInstantiation().getAPropertyWrite().getRhs().getALocalSource() or - // result.asInstantiation() = this.asInstantiation().getAnArgument() - // TrackParentControlConfig::flow(this.asInstantiation()) - /* =================================================================================== */ - } - - string getId() { - result = this.asInstantiation().getId() - or - /* TODO: Needs testing */ - result = - this.asDefinition() - .(CustomControl) - .getMetadata() - .getProperty("id") - .getAPropertySource() - .getStringValue() - /* - * Note that because we cannot statically determine the ID of an element from the references alone, - * we do not implement the branch of `TReferenceOfElement`. - */ - - } - - string toString() { - result = this.asDefinition().toString() or - result = this.asReference().toString() or - result = this.asInstantiation().toString() - } - - Location getLocation() { - result = this.asDefinition().getLocation() or - result = this.asReference().getLocation() or - result = this.asInstantiation().getLocation() - } -} - /** * The property metadata found in an SapExtendCall. */ diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll index f6cc00e34..a9503f76d 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/dataflow/DataFlow.qll @@ -229,15 +229,11 @@ module TrackPlaceAtCallConfig implements DataFlow::ConfigSig { */ predicate isSource(DataFlow::Node node) { node instanceof ElementInstantiation } - additional predicate isSinkWithPlaceAtCall(DataFlow::Node node, ControlPlaceAtCall placeAtCall) { - node = placeAtCall - } - /** * An "extension point" exposed from a parent element instantiation to * register a child to itself. */ - predicate isSink(DataFlow::Node node) { isSinkWithPlaceAtCall(node, _) } + predicate isSink(DataFlow::Node node) { node instanceof ControlPlaceAtCall } /** * Step from data being written and the property that is being written to. From 2c9e1b86595f8cafc63d1425f282bc077d4053a1 Mon Sep 17 00:00:00 2001 From: Jeongsoo Lee Date: Fri, 17 Oct 2025 16:25:54 -0400 Subject: [PATCH 10/17] Add unit test cases to test `DangerouslySetElementValueOfInstantiatedHTMLControlPlacedAtDom` --- .../dangerousWriteToHtmlContent.expected | 0 .../dangerousWriteToHtmlContent.ql | 5 ++ .../package-lock.json | 12 +++++ .../package.json | 5 ++ .../dangerous_write_to_html_content/ui5.yaml | 7 +++ .../webapp/controller/app.controller.js | 46 +++++++++++++++++++ .../webapp/index.html | 18 ++++++++ .../webapp/index.js | 11 +++++ .../webapp/manifest.json | 5 ++ .../webapp/view/app.view.xml | 7 +++ 10 files changed, 116 insertions(+) create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/dangerousWriteToHtmlContent.expected create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/dangerousWriteToHtmlContent.ql create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/package-lock.json create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/package.json create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/ui5.yaml create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/controller/app.controller.js create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/index.html create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/index.js create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/manifest.json create mode 100644 javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/view/app.view.xml diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/dangerousWriteToHtmlContent.expected b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/dangerousWriteToHtmlContent.expected new file mode 100644 index 000000000..e69de29bb diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/dangerousWriteToHtmlContent.ql b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/dangerousWriteToHtmlContent.ql new file mode 100644 index 000000000..3efaef5c4 --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/dangerousWriteToHtmlContent.ql @@ -0,0 +1,5 @@ +import javascript +import advanced_security.javascript.frameworks.ui5.UI5 +import advanced_security.javascript.frameworks.ui5.UI5View + +select "TODO", "TODO" diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/package-lock.json b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/package-lock.json new file mode 100644 index 000000000..ba052860f --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "sap-ui5-xss", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sap-ui5-xss", + "version": "1.0.0" + } + } +} diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/package.json b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/package.json new file mode 100644 index 000000000..9f0a4560b --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/package.json @@ -0,0 +1,5 @@ +{ + "name": "sap-ui5-xss", + "version": "1.0.0", + "main": "index.js" +} diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/ui5.yaml b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/ui5.yaml new file mode 100644 index 000000000..beb2ff69d --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/ui5.yaml @@ -0,0 +1,7 @@ +specVersion: '3.0' +metadata: + name: sap-ui5-xss +type: application +framework: + name: SAPUI5 + version: "1.115.0" diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/controller/app.controller.js b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/controller/app.controller.js new file mode 100644 index 000000000..926a5e9ea --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/controller/app.controller.js @@ -0,0 +1,46 @@ +sap.ui.define( + [ + "sap/ui/core/mvc/Controller", + "sap/m/Input", + "sap/m/Button", + "sap/m/VBox", + "sap/ui/core/HTML", + ], + function (Controller, Input, Button, VBox, HTML) { + "use strict"; + return Controller.extend("codeql-sap-js.controller.app", { + onInit: function () { + let inputReference = this.getView().byId("unit-test-target1"); + let htmlControl = this.getView().byId("htmlControl"); + + /* ========== 1. Input value piped into static HTML, via a reference ========== */ + /* 1-1. Value directly set to `HTML.content` */ + htmlControl.content = inputReference.getValue(); + + /* 1-2. Value set by `HTML.setContent(content)` */ + htmlControl.setContent(inputReference.getValue()); + }, + + doSomething: function () { + let inputReference = this.getView().byId("unit-test-target1"); + + /* ========== 2. Input value piped into dynamic HTML, instantiated and placed on-demand ========== */ + /* 2-1. Value passed to the argument of the constructor call */ + let htmlControl1 = new HTML({ + content: `
${inputReference.getValue()}
`, + }); + htmlControl1.placeAt("HTMLPlaceholder"); + + /* 2-2. Value directly set to `HTML.content` */ + let htmlControl2 = new HTML(); + htmlControl2.content = inputReference.getValue(); + htmlControl2.placeAt("HTMLPlaceholder"); + + /* 2-3. Value set by `HTML.setContent(content)` */ + let htmlControl3 = new HTML(); + htmlControl3.setContent(inputReference.getValue()); + htmlControl3.placeAt("HTMLPlaceholder"); + }, + }); + } +); diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/index.html b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/index.html new file mode 100644 index 000000000..c523033ec --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/index.html @@ -0,0 +1,18 @@ + + + + + SAPUI5 XSS + + + + +
+ diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/index.js b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/index.js new file mode 100644 index 000000000..7a66697fd --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/index.js @@ -0,0 +1,11 @@ +sap.ui.define([ + "sap/ui/core/mvc/XMLView" +], function (XMLView) { + "use strict"; + XMLView.create({ + viewName: "codeql-sap-js.view.app" + }).then(function (oView) { + oView.placeAt("content"); + }); + +}); \ No newline at end of file diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/manifest.json b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/manifest.json new file mode 100644 index 000000000..65d4de250 --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/manifest.json @@ -0,0 +1,5 @@ +{ + "sap.app": { + "id": "sap-ui5-xss" + } +} \ No newline at end of file diff --git a/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/view/app.view.xml b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/view/app.view.xml new file mode 100644 index 000000000..5f809cc7a --- /dev/null +++ b/javascript/frameworks/ui5/test/models/dangerous_write_to_html_content/webapp/view/app.view.xml @@ -0,0 +1,7 @@ + + +