diff --git a/javascript/frameworks/ui5/ext/ui5.model.yml b/javascript/frameworks/ui5/ext/ui5.model.yml index 7a02de61..68265d47 100644 --- a/javascript/frameworks/ui5/ext/ui5.model.yml +++ b/javascript/frameworks/ui5/ext/ui5.model.yml @@ -66,7 +66,6 @@ extensions: - ["UI5Control", "Member[getValue].ReturnValue", "remote"] - ["UI5CodeEditor", "Member[value]", "remote"] - ["UI5CodeEditor", "Member[getCurrentValue].ReturnValue", "remote"] - - ["global", "Member[jQuery].Member[sap].Member[getUriParameters].ReturnValue.Member[get].ReturnValue", "remote"] - ["global", "Member[jQuery].Member[sap].Member[syncHead,syncGet,syncGetText,syncPost,syncPostText].ReturnValue", "remote"] - ["UI5URIParameters", "Member[get].ReturnValue", "remote"] - ["UI5URIParameters", "Member[getAll].ReturnValue", "remote"] diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/Bindings.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/Bindings.qll index b799995f..dacd411c 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/Bindings.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/Bindings.qll @@ -193,7 +193,8 @@ abstract private class LateJavaScriptPropertyBinding extends DataFlow::Node { */ private predicate earlyPropertyBinding( DataFlow::NewNode newNode, DataFlow::PropWrite bindingTarget, DataFlow::Node binding, - DataFlow::Node bindingPath) { + DataFlow::Node bindingPath +) { // Property binding via an object literal binding with property `path`. // This assumes the value assigned to `path` is a binding, even if we cannot // statically determine it is a binding. @@ -205,9 +206,10 @@ private predicate earlyPropertyBinding( if exists(binding.getALocalSource()) then binding.getALocalSource() = bindingPath else binding = bindingPath // e.g., path: "/" + someVar - ) and not bindingPath.getStringValue() instanceof BindingString + ) and + not bindingPath.getStringValue() instanceof BindingString or - // Propery binding of an arbitrary property for which we can statically determined + // Property binding of an arbitrary property for which we can statically determined // the value written to the property is a binding path. exists(DataFlow::SourceNode objectLiteral | newNode.getAnArgument().getALocalSource() = objectLiteral and @@ -339,9 +341,7 @@ private newtype TBinding = * with a property `parts` assigned a value, or * an object literal that is assigned a string value that is a binding path. */ - TEarlyJavaScriptPropertyBinding( - DataFlow::PropWrite bindingTarget, DataFlow::ValueNode binding - ) { + TEarlyJavaScriptPropertyBinding(DataFlow::PropWrite bindingTarget, DataFlow::ValueNode binding) { earlyPropertyBinding(_, bindingTarget, binding, _) } or // Property binding via a call to `bindProperty` or `bindValue`. @@ -415,22 +415,24 @@ private newtype TBindingPath = ) } or TDynamicBindingPath(Binding binding, DataFlow::Node dynamicBinding, DataFlow::Node bindingPath) { - (exists(DataFlow::PropWrite bindingTarget | - binding = TEarlyJavaScriptPropertyBinding(bindingTarget, dynamicBinding) and - earlyPropertyBinding(_, bindingTarget, dynamicBinding, bindingPath) - ) - or - exists(LateJavaScriptPropertyBinding lateJavaScriptPropertyBinding | - // Property binding via a call to `bindProperty` or `bindValue`. - binding = TLateJavaScriptPropertyBinding(lateJavaScriptPropertyBinding, dynamicBinding) and - latePropertyBinding(lateJavaScriptPropertyBinding, dynamicBinding, bindingPath) - ) - or - exists(BindElementMethodCallNode bindElementMethodCall | - // Element binding via a call to `bindElement`. - binding = TLateJavaScriptContextBinding(bindElementMethodCall, dynamicBinding) and - lateContextBinding(bindElementMethodCall, dynamicBinding, bindingPath) - )) and + ( + exists(DataFlow::PropWrite bindingTarget | + binding = TEarlyJavaScriptPropertyBinding(bindingTarget, dynamicBinding) and + earlyPropertyBinding(_, bindingTarget, dynamicBinding, bindingPath) + ) + or + exists(LateJavaScriptPropertyBinding lateJavaScriptPropertyBinding | + // Property binding via a call to `bindProperty` or `bindValue`. + binding = TLateJavaScriptPropertyBinding(lateJavaScriptPropertyBinding, dynamicBinding) and + latePropertyBinding(lateJavaScriptPropertyBinding, dynamicBinding, bindingPath) + ) + or + exists(BindElementMethodCallNode bindElementMethodCall | + // Element binding via a call to `bindElement`. + binding = TLateJavaScriptContextBinding(bindElementMethodCall, dynamicBinding) and + lateContextBinding(bindElementMethodCall, dynamicBinding, bindingPath) + ) + ) and not dynamicBinding.mayHaveStringValue(_) } @@ -438,6 +440,9 @@ private newtype TBindingPath = * A class representing a binding path. */ class BindingPath extends TBindingPath { + /** + * For debugging purposes (pretty-printing in result table) + */ string toString() { exists(BindingStringParser::BindingPath path | this = TStaticBindingPath(_, _, path) and @@ -460,6 +465,11 @@ class BindingPath extends TBindingPath { this = TStaticBindingPath(_, _, path) and result = path.toString() ) + or + exists(DataFlow::Node pathValue | + this = TDynamicBindingPath(_, _, pathValue) and + result = pathValue.asExpr().(StringLiteral).getValue().regexpCapture("\\{(.*)\\}", 1) + ) } Location getLocation() { @@ -616,8 +626,8 @@ class BindingTarget extends TBindingTarget { Binding getBinding() { this = TXmlPropertyBindingTarget(_, result) or this = TXmlContextBindingTarget(_, result) or - this = TLateJavaScriptBindingTarget(_, result) or this = TEarlyJavaScriptPropertyBindingTarget(_, result) or + this = TLateJavaScriptBindingTarget(_, result) or this = TJsonPropertyBindingTarget(_, _, result) } } @@ -650,7 +660,9 @@ class Binding extends TBinding { or exists(DataFlow::PropWrite bindingTarget, DataFlow::Node binding | this = TEarlyJavaScriptPropertyBinding(bindingTarget, binding) and - result = "Early JavaScript property binding: " + bindingTarget.getPropertyNameExpr() + " to " + binding + result = + "Early JavaScript property binding: " + bindingTarget.getPropertyNameExpr() + " to " + + binding ) or exists(LateJavaScriptPropertyBinding lateJavaScriptPropertyBinding, DataFlow::Node binding | @@ -710,9 +722,7 @@ class Binding extends TBinding { ) } - BindingPath getBindingPath() { - result.getBinding() = this - } + BindingPath getBindingPath() { result.getBinding() = this } BindingTarget getBindingTarget() { result.getBinding() = this } } 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 new file mode 100644 index 00000000..b6423e12 --- /dev/null +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/RemoteFlowSources.qll @@ -0,0 +1,135 @@ +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 + +private class DataFromRemoteControlReference extends RemoteFlowSource, MethodCallNode { + DataFromRemoteControlReference() { + exists(UI5Control sourceControl, string typeAlias, ControlReference controlReference | + ApiGraphModelsExtensions::typeModel(typeAlias, sourceControl.getImportPath(), _) and + ApiGraphModelsExtensions::sourceModel(typeAlias, _, "remote") and + sourceControl.getAReference() = controlReference and + controlReference.flowsTo(this.getReceiver()) and + this.getMethodName() = "getValue" + ) + } + + override string getSourceType() { result = "Data from a remote control" } +} + +class LocalModelContentBoundBidirectionallyToSourceControl extends RemoteFlowSource { + UI5BindingPath bindingPath; + UI5Control controlDeclaration; + + LocalModelContentBoundBidirectionallyToSourceControl() { + exists(UI5InternalModel internalModel | + this = bindingPath.getNode() and + ( + this instanceof PropWrite and + internalModel.getArgument(0).getALocalSource().asExpr() = + this.(PropWrite).getPropertyNameExpr().getParent+() + or + this.asExpr() instanceof StringLiteral and + internalModel.asExpr() = this.asExpr().getParent() + ) and + any(UI5View view).getASource() = bindingPath and + internalModel.(JsonModel).isTwoWayBinding() and + controlDeclaration = bindingPath.getControlDeclaration() + ) + } + + override string getSourceType() { + result = "Local model bidirectionally bound to a input control" + } + + UI5BindingPath getBindingPath() { result = bindingPath } + + UI5Control getControlDeclaration() { result = controlDeclaration } +} + +abstract class UI5ExternalModel extends UI5Model, RemoteFlowSource { + abstract string getName(); +} + +/** Model which gains content from an SAP OData service. */ +class ODataServiceModel extends UI5ExternalModel { + string modelName; + + override string getSourceType() { result = "ODataServiceModel" } + + ODataServiceModel() { + /* + * e.g. this.getView().setModel(this.getOwnerComponent().getModel("booking_nobatch")) + */ + + 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"))` + */ + + 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 */ + controller.getOwnerComponent().getExternalModelDef(modelName).getDataSource() instanceof + ODataDataSourceManifest + ) + or + /* + * A constructor call to sap.ui.model.odata.v2.ODataModel. + */ + + this instanceof NewNode and + ( + exists(RequiredObject oDataModel | + oDataModel.flowsTo(this.getCalleeNode()) and + oDataModel.getDependencyType() = "sap/ui/model/odata/v2/ODataModel" + ) + or + this.getCalleeName() = "ODataModel" + ) and + modelName = "" + } + + override string getName() { result = modelName } +} + +private class RouteParameterAccess extends RemoteFlowSource instanceof PropRead { + override string getSourceType() { result = "RouteParameterAccess" } + + RouteParameterAccess() { + exists( + ControllerHandler handler, RouteManifest routeManifest, ParameterNode handlerParameter, + 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 + ( + routeManifest.matchesPathString(this.getPropertyName()) and + this.getBase().getALocalSource() = getParameterCall + or + /* TODO: Why does `routeManifest.matchesPathString` not work for propertyName?? */ + this.getBase().(PropRead).getBase().getALocalSource() = getParameterCall + ) + ) + } +} + +/** + * Method calls that fetch a piece of data either from a library control capable of accepting user input, or from a URI parameter. + */ +private class UI5ExtRemoteSource extends RemoteFlowSource { + UI5ExtRemoteSource() { this = ModelOutput::getASourceNode("remote").asSource() } + + override string getSourceType() { + result = "Remote flow" // Don't discriminate between UI5-specific remote flows and vanilla ones + } +} 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 be20af23..7ac0c6b9 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 @@ -1,612 +1,1174 @@ -private import javascript -private import DataFlow -private import advanced_security.javascript.frameworks.ui5.JsonParser -private import semmle.javascript.security.dataflow.DomBasedXssCustomizations -private import advanced_security.javascript.frameworks.ui5.UI5View -private import advanced_security.javascript.frameworks.ui5.UI5HTML - -module UI5 { - private module WebAppResourceRootJsonReader implements JsonParser::MakeJsonReaderSig { - class JsonReader extends WebApp{ - string getJson() { - // We match on the lowercase to cover all the possible variants of wrtiting the attribute name. - exists(string resourceRootAttributeName | resourceRootAttributeName.toLowerCase() = "data-sap-ui-resourceroots" | - result = this.getCoreScript().getAttributeByName(resourceRootAttributeName).getValue() - ) - } +import javascript +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.UI5HTML + +private module WebAppResourceRootJsonReader implements JsonParser::MakeJsonReaderSig { + class JsonReader extends WebApp { + string getJson() { + // We match on the lowercase to cover all the possible variants of writing the attribute name. + exists(string resourceRootAttributeName | + resourceRootAttributeName.toLowerCase() = "data-sap-ui-resourceroots" + | + result = this.getCoreScript().getAttributeByName(resourceRootAttributeName).getValue() + ) } } +} - private module WebAppResourceRootJsonParser = JsonParser::Make; +private module WebAppResourceRootJsonParser = + JsonParser::Make; + +private predicate isAnUnResolvedResourceRoot(WebApp webApp, string name, string path) { + exists( + WebAppResourceRootJsonParser::JsonObject config, + WebAppResourceRootJsonParser::JsonMember configEntry + | + config.getReader() = webApp and + config.getAMember() = configEntry and + name = configEntry.getKey() and + path = configEntry.getValue().asString() + ) +} - private predicate isAnUnResolvedResourceRoot(WebApp webApp, string name, string path) { - exists( - WebAppResourceRootJsonParser::JsonObject config, - WebAppResourceRootJsonParser::JsonMember configEntry - | - config.getReader() = webApp and - config.getAMember() = configEntry and - name = configEntry.getKey() and - path = configEntry.getValue().asString() - ) - } +class ResourceRootPathString extends PathString { + WebApp webApp; - class ResourceRootPathString extends PathString { - WebApp webApp; - ResourceRootPathString() { - isAnUnResolvedResourceRoot(webApp, _, this) - } + ResourceRootPathString() { isAnUnResolvedResourceRoot(webApp, _, this) } - override Folder getARootFolder() { - result = webApp.getWebAppFolder() - } + override Folder getARootFolder() { result = webApp.getWebAppFolder() } +} + +class ResourceRoot extends Container { + string name; + string path; + WebApp webApp; + + ResourceRoot() { + isAnUnResolvedResourceRoot(webApp, name, path) and + path.(PathString).resolve(webApp.getWebAppFolder()).getContainer() = this } - class ResourceRoot extends Container { - string name; - string path; - WebApp webApp; - ResourceRoot() { - isAnUnResolvedResourceRoot(webApp, name, path) - and - path.(PathString).resolve(webApp.getWebAppFolder()).getContainer() = this - } + string getName() { result = name } - string getName() { result = name } + WebApp getWebApp() { result = webApp } - WebApp getWebApp() { result = webApp} + predicate contains(File file) { this.getAChildContainer+().getAFile() = file } +} - predicate contains(File file) { - this.getAChildContainer+().getAFile() = file - } +class SapUiCoreScriptElement extends HTML::ScriptElement { + SapUiCoreScriptElement() { + this.getSourcePath().matches(["%sap-ui-core.js", "%sap-ui-core-nojQuery.js"]) } - class SapUiCoreScriptElement extends HTML::ScriptElement { - SapUiCoreScriptElement() { - this.getSourcePath().matches(["%sap-ui-core.js", "%sap-ui-core-nojQuery.js"]) - } + WebApp getWebApp() { result = this.getFile() } +} - WebApp getWebApp() { result = this.getFile() } +/** A UI5 web application manifest associated with a bootstrapped UI5 web application. */ +class WebAppManifest extends File { + WebApp webapp; + + WebAppManifest() { + this.getBaseName() = "manifest.json" and + this.getParentContainer() = webapp.getWebAppFolder() } - /** A UI5 web application manifest associated with a bootstrapped UI5 web application. */ - class WebAppManifest extends File { - WebApp webapp; + WebApp getWebapp() { result = webapp } +} - WebAppManifest() { - this.getBaseName() = "manifest.json" and - this.getParentContainer() = webapp.getWebAppFolder() - } +/** A UI5 bootstrapped web application. */ +class WebApp extends HTML::HtmlFile { + SapUiCoreScriptElement coreScript; + + WebApp() { coreScript.getFile() = this } + + SapUiCoreScriptElement getCoreScript() { result = coreScript } - WebApp getWebapp() { result = webapp } + ResourceRoot getAResourceRoot() { result.getWebApp() = this } + + File getAResource() { getAResourceRoot().contains(result) } + + File getResource(string relativePath) { + result.getAbsolutePath() = getAResourceRoot().getAbsolutePath() + "/" + relativePath } - /** A UI5 bootstrapped web application. */ - class WebApp extends HTML::HtmlFile { - SapUiCoreScriptElement coreScript; + Folder getWebAppFolder() { result = this.getParentContainer() } - WebApp() { coreScript.getFile() = this } + WebAppManifest getManifest() { result.getWebapp() = this } - SapUiCoreScriptElement getCoreScript() { result = coreScript } + /** + * Gets the JavaScript module that serves as an entrypoint to this webapp. + */ + File getInitialModule() { + exists(string initialModuleResourcePath, string resolvedModulePath, ResourceRoot resourceRoot | + initialModuleResourcePath = coreScript.getAttributeByName("data-sap-ui-onInit").getValue() and + resourceRoot.getWebApp() = this and + resolvedModulePath = + initialModuleResourcePath + .regexpReplaceAll("^module\\s*:\\s*", "") + .replaceAll(resourceRoot.getName(), resourceRoot.getAbsolutePath()) and + result.getAbsolutePath() = resolvedModulePath + ".js" + ) + } - ResourceRoot getAResourceRoot() { - result.getWebApp() = this - } + FrameOptions getFrameOptions() { + exists(HTML::DocumentElement doc | doc.getFile() = this | + result.asHtmlFrameOptions() = coreScript.getAnAttribute() + ) + or + result.asJsFrameOptions().getFile() = this + } - File getAResource() { getAResourceRoot().contains(result)} + HTML::DocumentElement getDocument() { result.getFile() = this } +} - File getResource(string relativePath) { - result.getAbsolutePath() = getAResourceRoot().getAbsolutePath() + "/" + relativePath - } +/** + * https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.loader%23methods/sap.ui.loader.config + */ +class Loader extends CallNode { + Loader() { this = globalVarRef("sap").getAPropertyRead("ui").getAMethodCall("loader") } +} - Folder getWebAppFolder() { result = this.getParentContainer() } +/** + * A user-defined module through `sap.ui.define` or `jQuery.sap.declare`. + */ +abstract class UserModule extends InvokeNode { + abstract string getADependencyType(); - WebAppManifest getManifest() { result.getWebapp() = this } + abstract string getModuleFileRelativePath(); - /** - * Gets the JavaScript module that serves as an entrypoint to this webapp. - */ - File getInitialModule() { - exists( - string initialModuleResourcePath, string resolvedModulePath, - ResourceRoot resourceRoot - | - initialModuleResourcePath = coreScript.getAttributeByName("data-sap-ui-onInit").getValue() and - resourceRoot.getWebApp() = this and - resolvedModulePath = - initialModuleResourcePath - .regexpReplaceAll("^module\\s*:\\s*", "") - .replaceAll(resourceRoot.getName(), resourceRoot.getAbsolutePath()) and - result.getAbsolutePath() = resolvedModulePath + ".js" - ) - } + abstract RequiredObject getRequiredObject(string dependencyType); +} - FrameOptions getFrameOptions() { - exists(HTML::DocumentElement doc | doc.getFile() = this | - result.asHtmlFrameOptions() = coreScript.getAnAttribute() - ) - or - result.asJsFrameOptions().getFile() = this - } +/** + * A user-defined module through `sap.ui.define`. + * https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui%23methods/sap.ui.define + */ +class SapDefineModule extends CallNode, UserModule { + SapDefineModule() { this = globalVarRef("sap").getAPropertyRead("ui").getAMethodCall("define") } - HTML::DocumentElement getDocument() { - result.getFile() = this - } + override string getADependencyType() { result = this.getDependencyType(_) } + + override string getModuleFileRelativePath() { result = this.getFile().getRelativePath() } + + string getDependencyType(int i) { + result = this.getArgument(0).getALocalSource().(ArrayLiteralNode).getElement(i).getStringValue() } - /** - * https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.loader%23methods/sap.ui.loader.config - */ - class Loader extends CallNode { - Loader() { this = globalVarRef("sap").getAPropertyRead("ui").getAMethodCall("loader") } + override RequiredObject getRequiredObject(string dependencyType) { + exists(int i | + this.getDependencyType(i) = dependencyType and + result = this.getArgument(1).getALocalSource().(FunctionNode).getParameter(i) + ) } - /** - * A user-defined module through `sap.ui.define` or `jQuery.sap.declare`. - */ - abstract class UserModule extends InvokeNode { - abstract string getADependencyType(); + WebApp getWebApp() { this.getFile() = result.getAResource() } - abstract string getModuleFileRelativePath(); + SapDefineModule getExtendingDefine() { + exists(Extension baseExtension, Extension subclassExtension, SapDefineModule subclassDefine | + baseExtension.getDefine() = this and + subclassDefine = subclassExtension.getDefine() and + any(RequiredObject module_ | + module_ = subclassDefine.getRequiredObject(baseExtension.getName().replaceAll(".", "/")) + ).flowsTo(subclassExtension.getReceiver()) and + result = subclassDefine + ) + } +} - abstract RequiredObject getRequiredObject(string dependencyType); +class JQuerySap extends DataFlow::SourceNode { + JQuerySap() { + exists(DataFlow::GlobalVarRefNode global | + global.getName() = "jQuery" and + this = global.getAPropertyRead("sap") + ) } +} - /** - * A user-defined module through `sap.ui.define`. - * https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui%23methods/sap.ui.define - */ - class SapDefineModule extends CallNode, UserModule { - SapDefineModule() { this = globalVarRef("sap").getAPropertyRead("ui").getAMethodCall("define") } +/** + * A user-defined module through `jQuery.sap.declare`. + */ +class JQueryDefineModule extends UserModule, DataFlow::MethodCallNode { + JQueryDefineModule() { exists(JQuerySap jquerySap | jquerySap.flowsTo(this.getReceiver())) } - override string getADependencyType() { result = this.getDependencyType(_) } + override string getADependencyType() { + result = this.getArgument(0).asExpr().(StringLiteral).getValue() + } - override string getModuleFileRelativePath() { result = this.getFile().getRelativePath() } + override string getModuleFileRelativePath() { result = this.getFile().getRelativePath() } - string getDependencyType(int i) { - result = - this.getArgument(0).getALocalSource().(ArrayLiteralNode).getElement(i).getStringValue() - } + /** WARNING: toString() Hack! */ + override RequiredObject getRequiredObject(string dependencyType) { + result.toString() = dependencyType and + this.getADependencyType() = dependencyType + } +} - override RequiredObject getRequiredObject(string dependencyType) { - exists(int i | - this.getDependencyType(i) = dependencyType and - result = this.getArgument(1).getALocalSource().(FunctionNode).getParameter(i) - ) - } +private RequiredObject sapControl(TypeTracker t) { + t.start() and + exists(UserModule d, string dependencyType | + dependencyType = ["sap/ui/core/Control", "sap.ui.core.Control"] + | + d.getADependencyType() = dependencyType and + result = d.getRequiredObject(dependencyType) + ) + or + exists(TypeTracker t2 | result = sapControl(t2).track(t2, t)) +} - WebApp getWebApp() { this.getFile() = result.getAResource() } +private SourceNode sapControl() { result = sapControl(TypeTracker::end()) } + +private SourceNode sapController(TypeTracker t) { + t.start() and + exists(UserModule d, string dependencyType | + dependencyType = ["sap/ui/core/mvc/Controller", "sap.ui.core.mvc.Controller"] + | + d.getADependencyType() = dependencyType and + result = d.getRequiredObject(dependencyType) + ) + or + exists(TypeTracker t2 | result = sapController(t2).track(t2, t)) +} - SapDefineModule getExtendingDefine() { - exists(Extension baseExtension, Extension subclassExtension, SapDefineModule subclassDefine | - baseExtension.getDefine() = this and - subclassDefine = subclassExtension.getDefine() and - any(RequiredObject module_ | - module_ = subclassDefine.getRequiredObject(baseExtension.getName().replaceAll(".", "/")) - ).flowsTo(subclassExtension.getReceiver()) and - result = subclassDefine - ) - } +private SourceNode sapController() { result = sapController(TypeTracker::end()) } + +class CustomControl extends Extension { + CustomControl() { + this.getReceiver().getALocalSource() = sapControl() or + this.getDefine() = any(SapDefineModule sapModule).getExtendingDefine() } - class JQuerySap extends DataFlow::SourceNode { - JQuerySap() { - exists(DataFlow::GlobalVarRefNode global | - global.getName() = "jQuery" and - this = global.getAPropertyRead("sap") - ) - } + MethodCallNode getOwnerComponentRef() { + exists(ThisNode controlThis | + controlThis.getBinder() = this.getAMethod() and + controlThis.flowsTo(result.getReceiver()) and + result.getMethodName() = "getOwnerComponent" + ) } - /** - * A user-defined module through `jQuery.sap.declare`. - */ - class JQueryDefineModule extends UserModule, DataFlow::MethodCallNode { - JQueryDefineModule() { exists(JQuerySap jquerySap | jquerySap.flowsTo(this.getReceiver())) } + CustomController getController() { this = result.getAControlReference().getDefinition() } - override string getADependencyType() { - result = this.getArgument(0).asExpr().(StringLiteral).getValue() - } + UI5Control getAViewUsage() { result.getDefinition() = this } +} - override string getModuleFileRelativePath() { result = this.getFile().getRelativePath() } +abstract class Reference extends MethodCallNode { } - /** WARNING: toString() Hack! */ - override RequiredObject getRequiredObject(string dependencyType) { - result.toString() = dependencyType and - this.getADependencyType() = dependencyType - } - } +/** + * A JS reference to a `UI5Control`, commonly obtained via `View.byId(controlId)`. + */ +class ControlReference extends Reference { + string controlId; - private RequiredObject sapControl(TypeTracker t) { - t.start() and - exists(UserModule d, string dependencyType | - dependencyType = ["sap/ui/core/Control", "sap.ui.core.Control"] - | - d.getADependencyType() = dependencyType and - result = d.getRequiredObject(dependencyType) + ControlReference() { + exists(CustomController controller | + controller.getAViewReference().flowsTo(this.getReceiver()) and + this.getMethodName() = "byId" and + this.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() = controlId ) - or - exists(TypeTracker t2 | result = sapControl(t2).track(t2, t)) } - SourceNode sapControl() { result = sapControl(TypeTracker::end()) } - - private SourceNode sapController(TypeTracker t) { - t.start() and - exists(UserModule d, string dependencyType | - dependencyType = ["sap/ui/core/mvc/Controller", "sap.ui.core.mvc.Controller"] - | - d.getADependencyType() = dependencyType and - result = d.getRequiredObject(dependencyType) + CustomControl getDefinition() { + exists(UI5Control controlDeclaration | + this = controlDeclaration.getAReference() and + result = controlDeclaration.getDefinition() ) - or - exists(TypeTracker t2 | result = sapController(t2).track(t2, t)) } - SourceNode sapController() { result = sapController(TypeTracker::end()) } + string getId() { result = controlId } +} - class CustomControl extends Extension { - CustomControl() { - this.getReceiver().getALocalSource() = sapControl() or - this.getDefine() = any(SapDefineModule sapModule).getExtendingDefine() - } +/** + * A reference to a `UI5View`, commonly obtained via `Controller.getView()`. + */ +class ViewReference extends Reference { + CustomController controller; + + ViewReference() { + this.getMethodName() = "getView" and + controller.getAThisNode().flowsTo(this.getReceiver()) } - class CustomController extends Extension { - CustomController() { - this instanceof MethodCallNode and this.getReceiver().getALocalSource() = sapController() - } + UI5View getDefinition() { result = controller.getView() } - FunctionNode getAMethod() { - result = this.getArgument(1).(ObjectLiteralNode).getAPropertySource().(FunctionNode) - } + MethodCallNode getABindElementCall() { + result.getMethodName() = "bindElement" and + this.flowsTo(result.getReceiver()) + } +} - /** - * Gets a reference to a view object that can be accessed from one of the methods of this controller. - */ - MethodCallNode getAViewReference() { - result.getCalleeName() = "getView" and - exists(ThisNode controllerThis | - result.(MethodCallNode).getReceiver() = controllerThis.getALocalUse() and - controllerThis.getBinder() = this.getAMethod() - ) - } +/** + * A reference to a CustomController, commonly obtained via `View.getController()`. + */ +class ControllerReference extends Reference { + ViewReference viewReference; - MethodCallNode getAnElementReference() { - 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" - ) - } + ControllerReference() { viewReference.flowsTo(this.getReceiver()) } - ThisNode getAThisNode() { result.getBinder() = this.getAMethod() } + CustomController getDefinition() { result = viewReference.getDefinition().getController() } +} - UI5Model getModel() { - exists(MethodCallNode setModelCall | - this.getAViewReference().flowsTo(setModelCall.getReceiver()) and - setModelCall.getMethodName() = "setModel" and - result.flowsTo(setModelCall.getAnArgument()) - ) - } +class CustomController extends Extension { + string name; - MethodCallNode getAModelReference() { - result.getMethodName() = "getModel" and - this.getAViewReference().flowsTo(result.getReceiver()) - } + CustomController() { + this.getReceiver().getALocalSource() = sapController() and + name = this.getFile().getBaseName().regexpCapture("([a-zA-Z0-9]+).[cC]ontroller.js", 1) } - abstract class UI5Model extends InvokeNode { - abstract string getPathString(); - - CustomController getController() { result.asExpr() = this.asExpr().getParent+() } + Component getOwnerComponent() { + exists(ManifestJson manifestJson, JsonObject rootObj | manifestJson = result.getManifestJson() | + rootObj + .getPropValue("targets") + .(JsonObject) + // The individual targets + .getPropValue(_) + .(JsonObject) + // The target's "viewName" property + .getPropValue("viewName") + .(JsonString) + .getValue() = name + ) } - private string constructPathStringInner(Expr object) { - if not object instanceof ObjectExpr - then result = "" - else - exists(Property property | property = object.(ObjectExpr).getAProperty().(ValueProperty) | - result = "/" + property.getName() + constructPathStringInner(property.getInit()) - ) + MethodCallNode getOwnerComponentRef() { + exists(ThisNode controlThis | + controlThis.getBinder() = this.getAMethod() and + controlThis.flowsTo(result.getReceiver()) and + result.getMethodName() = "getOwnerComponent" + ) } /** - * Create all recursive path strings of an object literal, e.g. - * if `object = { p1: { p2: 1 }, p3: 2 }`, then create: - * - `p1/p2`, and - * - `p3/`. + * Gets a reference to a view object that can be accessed from one of the methods of this controller. */ - private string constructPathString(DataFlow::ObjectLiteralNode object) { - result = constructPathStringInner(object.asExpr()) + ViewReference getAViewReference() { + exists(ThisNode controllerThis | + result.getMethodName() = "getView" and + result.(MethodCallNode).getReceiver() = controllerThis.getALocalUse() and + controllerThis.getBinder() = this.getAMethod() + ) } - /** Holds if the `property` is in any way nested inside the `object`. */ - private predicate propertyNestedInObject(ObjectExpr object, Property property) { - exists(Property property2 | property2 = object.getAProperty() | - property = property2 or - propertyNestedInObject(property2.getInit().(ObjectExpr), property) + 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" ) } - private string constructPathStringInner(Expr object, Property property) { - if not object instanceof ObjectExpr - then result = "" - else - exists(Property property2 | property2 = object.(ObjectExpr).getAProperty().(ValueProperty) | - if property = property2 - then result = "/" + property2.getName() - else ( - /* We're sure this property is inside this object */ - propertyNestedInObject(property2.getInit().(ObjectExpr), property) and - result = - "/" + property2.getName() + constructPathStringInner(property2.getInit(), property) - ) - ) + ThisNode getAThisNode() { result.getBinder() = this.getAMethod() } + + UI5Model getModel() { + exists(MethodCallNode setModelCall | + this.getAViewReference().flowsTo(setModelCall.getReceiver()) and + setModelCall.getMethodName() = "setModel" and + result.flowsTo(setModelCall.getAnArgument()) + ) + } + + ModelReference getAModelReference() { this.getAViewReference().flowsTo(result.getReceiver()) } + + RouterReference getARouterReference() { + result.getMethodName() = "getRouter" and + exists(ThisNode controllerThis | + result.(MethodCallNode).getReceiver() = controllerThis.getALocalUse() and + controllerThis.getBinder() = this.getAMethod() + ) + } + + ControllerHandler getHandler(string handlerName) { + result = this.getContent().getAPropertySource(handlerName) + } + + ControllerHandler getAHandler() { result = this.getHandler(_) } +} + +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())) + } + + string getName() { result = name } +} + +class ControllerHandler extends FunctionNode { + string name; + CustomController controller; + + ControllerHandler() { this = controller.getContent().getAPropertySource(name).(FunctionNode) } + + override string getName() { result = name } + + predicate isAttachedToRoute(string routeName) { + exists(MethodCallNode attachMatchedCall, RouteReference routeReference | + routeReference.getName() = routeName and + routeReference.flowsTo(attachMatchedCall.getReceiver()) and + attachMatchedCall.getMethodName() = "attachMatched" and + attachMatchedCall.getArgument(0).(PropRead).getPropertyName() = name + ) + } +} + +class RouterReference extends MethodCallNode { + RouterReference() { + this.getMethodName() = "getRouter" and + exists(CustomController controller | controller.getAThisNode().flowsTo(this.getReceiver())) + } +} + +/** + * A reference to a model obtained by a method call to `getModel`. + */ +class ModelReference extends MethodCallNode { + ModelReference() { + this.getMethodName() = "getModel" and + exists(ViewReference view | view.flowsTo(this.getReceiver())) } + predicate isDefaultModelReference() { this.getNumArgument() = 0 } + /** - * Create all possible path strings of an object literal up to a certain property, e.g. - * if `object = { p1: { p2: 1 }, p3: 2 }` and `property = {p3: 2}` then create `"p3/"`. + * Gets the models' name being referred to, given that it can be statically determined. */ - string constructPathString(DataFlow::ObjectLiteralNode object, Property property) { - result = constructPathStringInner(object.asExpr(), property) + string getModelName() { + result = this.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() + } + + predicate isLocalModelReference() { + exists(InternalModelManifest internalModelManifest | + internalModelManifest.getName() = this.getModelName() + ) or + this.getResolvedModel() instanceof UI5InternalModel } /** - * Create all recursive path strings of a JSON object, e.g. - * if `object = { "p1": { "p2": 1 }, "p3": 2 }`, then create: - * - `/p1/p2`, and - * - `/p3`. + * Gets the matching `setModel` method call of this `ModelReference`. */ - string constructPathStringJson(JsonValue object) { - if not object instanceof JsonObject - then result = "" - else - exists(string property | exists(object.(JsonObject).getPropValue(property)) | - result = "/" + property + constructPathStringJson(object.getPropValue(property)) + MethodCallNode getAMatchingSetModelCall() { + exists(MethodCallNode setModelCall | + setModelCall.getMethodName() = "setModel" and + result = setModelCall and + ( + if this.isDefaultModelReference() + then ( + /* ========== A nameless default model ========== */ + setModelCall.getNumArgument() = 1 and + /* 1. A matching `setModel` call is on a `ViewReference` */ + exists(ViewReference getModelCallViewRef, ViewReference setModelCallViewRef | + /* Find the `setModelCall` that matches this */ + setModelCall.getReceiver().getALocalSource() = setModelCallViewRef and + this.getReceiver().getALocalSource() = getModelCallViewRef and + setModelCallViewRef.getDefinition() = getModelCallViewRef.getDefinition() + ) + or + /* 2. A matching `setModel` call is on a `ControlReference` */ + exists(ControlReference getModelCallControlRef, ControlReference setModelCallControlRef | + /* Find the `setModelCall` that matches this */ + setModelCall.getReceiver().getALocalSource() = setModelCallControlRef and + this.getReceiver().getALocalSource() = getModelCallControlRef and + ( + setModelCallControlRef.getDefinition() = getModelCallControlRef.getDefinition() or + setModelCallControlRef.getId() = getModelCallControlRef.getId() + ) + ) + ) else ( + /* ========== A named non-default model ========== */ + setModelCall.getNumArgument() = 2 and + setModelCall.getArgument(1).getALocalSource().getStringValue() = this.getModelName() and + /* 1. A matching `setModel` call is on a `ViewReference` */ + exists(ViewReference getModelCallViewRef, ViewReference setModelCallViewRef | + /* Find the `setModelCall` that matches this */ + setModelCall.getReceiver().getALocalSource() = setModelCallViewRef and + this.getReceiver().getALocalSource() = getModelCallViewRef and + setModelCallViewRef.getDefinition() = getModelCallViewRef.getDefinition() + ) + or + /* 2. A matching `setModel` call is on a `ControlReference` */ + exists(ControlReference getModelCallControlRef, ControlReference setModelCallControlRef | + /* Find the `setModelCall` that matches this */ + setModelCall.getReceiver().getALocalSource() = setModelCallControlRef and + this.getReceiver().getALocalSource() = getModelCallControlRef and + ( + setModelCallControlRef.getDefinition() = getModelCallControlRef.getDefinition() or + setModelCallControlRef.getId() = getModelCallControlRef.getId() + ) + ) + ) ) + ) } /** - * Create all possible path strings of a JSON object up to a certain property name, e.g. - * if `object = { "p1": { "p2": 1 }, "p3": 2 }` and `propName = "p3"` then create `"/p3"`. - * PRECONDITION: All of `object`'s keys are unique. + * Gets a `getProperty` or `getObject` method call on this `ModelReference`. These methods read from a single property of the model this refers to. */ - bindingset[propName] - string constructPathStringJson(JsonValue object, string propName) { - exists(string pathString | pathString = constructPathStringJson(object) | - pathString.regexpMatch(propName) and - result = pathString - ) + MethodCallNode getARead() { + result.getMethodName() = ["getProperty", "getObject"] and + result.getReceiver().getALocalSource() = this } /** - * When given a constructor call `new JSONModel("controller/model.json")`, - * get the content of the file referred to by URI (`"controller/model.json"`) - * inside the string argument. + * Gets the resolved model of this `ModelReference` by looking for a matching `setModel` call. */ - bindingset[path] - JsonObject resolveDirectPath(string path) { - exists(WebApp webApp | result.getJsonFile() = webApp.getResource(path)) + UI5Model getResolvedModel() { + /* TODO: If the argument of the setModelCall is another ModelReference, then we should recursively resolve that */ + result = this.getAMatchingSetModelCall().getArgument(0).getALocalSource() } +} + +abstract class UI5Model extends InvokeNode { + CustomController getController() { result.asExpr() = this.asExpr().getParent+() } /** - * When given a constructor call `new JSONModel(sap.ui.require.toUrl("sap/ui/demo/mock/products.json")`, - * get the content of the file referred to by resolving the argument. - * Currently only supports `sap.ui.require.toUrl`. + * A `getProperty` or `getObject` method call on this `UI5Model`. These methods read from a single property of this model. */ - bindingset[path] - JsonObject resolveIndirectPath(string path) { - result = any(JsonObject tODO | tODO.getFile().getAbsolutePath() = path) + MethodCallNode getARead() { + result.getMethodName() = ["getProperty", "getObject"] and + result.getReceiver().getALocalSource() = this } +} - class JsonModel extends UI5Model { - JsonModel() { - this instanceof NewNode and - exists(RequiredObject jsonModel | - jsonModel.flowsTo(this.getCalleeNode()) and - jsonModel.getDependencyType() = "sap/ui/model/json/JSONModel" - ) - } +/** + * Represents models that are loaded from an internal source, i.e. XML Models or JSON models + * whose contents are hardcoded in a JS file or loaded from a JSON file. + * It is always the constructor call that creates the model. + */ +abstract class UI5InternalModel extends UI5Model, NewNode { + abstract string getPathString(); - override string getPathString() { - /* 1. new JSONModel("controller/model.json") */ - if this.getAnArgument().asExpr() instanceof StringLiteral - then - result = - constructPathStringJson(resolveDirectPath(this.getAnArgument() - .asExpr() - .(StringLiteral) - .getValue())) - else - if this.getAnArgument().(MethodCallNode).getAnArgument().asExpr() instanceof StringLiteral - then - /* 2. new JSONModel(sap.ui.require.toUrl("sap/ui/demo/mock/products.json")) */ - result = - constructPathStringJson(resolveIndirectPath(this.getAnArgument() - .(MethodCallNode) - .getAnArgument() - .asExpr() - .(StringLiteral) - .getValue())) - else - /* - * 3. new JSONModel(oData) where - * var oData = { input: null }; - */ - - exists(ObjectLiteralNode objectNode | - objectNode.flowsTo(this.getAnArgument()) and constructPathString(objectNode) = result - ) - } + abstract string getPathString(Property property); +} + +/** + * 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. + */ +private SourceNode sapComponent(TypeTracker t) { + t.start() and + exists(UserModule d, string dependencyType | + dependencyType = + [ + "sap/ui/core/mvc/Component", "sap.ui.core.mvc.Component", "sap/ui/core/UIComponent", + "sap.ui.core.UIComponent" + ] + | + d.getADependencyType() = dependencyType and + result = d.getRequiredObject(dependencyType) + ) + or + exists(TypeTracker t2 | result = sapComponent(t2).track(t2, t)) +} + +private SourceNode sapComponent() { result = sapComponent(TypeTracker::end()) } + +import ManifestJson + +/** + * A UI5 Component that may contain other controllers or controls. + */ +class Component extends Extension { + Component() { this.getReceiver().getALocalSource() = sapComponent() } + + string getId() { result = this.getName().regexpCapture("([a-zA-Z0-9.]+).Component", 1) } + + ManifestJson getManifestJson() { + this.getMetadata().getAPropertySource("manifest").asExpr().(StringLiteral).getValue() = "json" and + result.getId() = this.getId() + } + + /** Get a definition of this component's model whose data source is remote. */ + DataSourceManifest getADataSource() { result = this.getADataSource(_) } + + /** Get a definition of this component's model whose data source is remote and is called modelName. */ + DataSourceManifest getADataSource(string modelName) { result.getName() = modelName } + + /** Get a reference to this component's external model. */ + MethodCallNode getAnExternalModelRef() { result = this.getAnExternalModelRef(_) } + + /** Get a reference to this component's external model called `modelName`. */ + MethodCallNode getAnExternalModelRef(string modelName) { + result.getMethodName() = "getModel" and + result.getArgument(0).asExpr().(StringLiteral).getValue() = modelName and + exists(ExternalModelManifest externModelDef | externModelDef.getName() = modelName) + } + + ExternalModelManifest getExternalModelDef(string modelName) { + result.getFile() = this.getManifestJson() and result.getName() = modelName + } - string getPathString(Property property) { - /* - * 3. new JSONModel(oData) where - * var oData = { input: null }; - */ + ExternalModelManifest getAnExternalModelDef() { result = this.getExternalModelDef(_) } +} + +module ManifestJson { + class DataSourceManifest extends JsonObject { + string dataSourceName; + ManifestJson manifestJson; - exists(ObjectLiteralNode objectNode | - objectNode.flowsTo(this.getAnArgument()) and - constructPathString(objectNode, property) = result + DataSourceManifest() { + exists(JsonObject rootObj | + this.getJsonFile() = manifestJson and + rootObj.getJsonFile() = manifestJson and + this = + rootObj + .getPropValue("sap.app") + .(JsonObject) + .getPropValue("dataSources") + .(JsonObject) + .getPropValue(dataSourceName) ) } - bindingset[propName] - string getPathStringPropName(string propName) { - exists(JsonObject jsonObject | - jsonObject = resolveDirectPath(this.getAnArgument().asExpr().(StringLiteral).getValue()) - | - constructPathStringJson(jsonObject, propName) = result + string getName() { result = dataSourceName } + + ManifestJson getManifestJson() { result = manifestJson } + + string getType() { result = this.getPropValue("type").(JsonString).getValue() } + } + + class ODataDataSourceManifest extends DataSourceManifest { + ODataDataSourceManifest() { this.getType() = "OData" } + } + + class JsonDataSourceDefinition extends DataSourceManifest { + JsonDataSourceDefinition() { this.getType() = "JSON" } + } + + class RouterManifest extends JsonObject { + ManifestJson manifestJson; + + RouterManifest() { + exists(JsonObject rootObj | + this.getJsonFile() = manifestJson and + rootObj.getJsonFile() = manifestJson and + this = rootObj.getPropValue("sap.ui5").(JsonObject).getPropValue("routing") ) } + RouteManifest getRoute() { result = this.getPropValue("routes").getElementValue(_) } + } + + class RouteManifest extends JsonObject { + RouterManifest parentRouterManifest; + + RouteManifest() { this = parentRouterManifest.getPropValue("routes").getElementValue(_) } + + string getPattern() { result = this.getPropStringValue("pattern") } + /** - * A model possibly supporting two-way binding explicitly set as a one-way binding model. + * Holds if, e.g., `this.getPattern() = "somePath/{someSuffix}"` and `path = "someSuffix"` */ - predicate isOneWayBinding() { - exists(MethodCallNode call, BindingMode bindingMode | - this.flowsTo(call.getReceiver()) and - call.getMethodName() = "setDefaultBindingMode" and - bindingMode.getOneWay().flowsTo(call.getArgument(0)) - ) + predicate matchesPathString(string path) { + path = this.getPattern().regexpCapture("([a-zA-Z]+/)\\{(.*)\\}.*", 2) } + + string getName() { result = this.getPropStringValue("name") } + + string getTarget() { result = this.getPropStringValue("target") } } - class XmlModel extends UI5Model { - XmlModel() { - this instanceof NewNode and - exists(RequiredObject xmlModel | - xmlModel.flowsTo(this.getCalleeNode()) and - xmlModel.getDependencyType() = "sap/ui/model/xml/XMLModel" + abstract class ModelManifest extends JsonObject { } + + class InternalModelManifest extends ModelManifest { + string modelName; + string type; + + InternalModelManifest() { + exists(JsonObject models, JsonObject modelsParent | + models = modelsParent.getPropValue("models") and + this = models.getPropValue(modelName) and + type = this.getPropStringValue("type") and + this.getPropStringValue("type") = + [ + "sap.ui.model.json.JSONModel", // A JSON Model + "sap.ui.model.xml.XMLModel", // An XML Model + "sap.ui.model.resource.ResourceModel" // A Resource Model, typically for i18n + ] ) } - override string getPathString() { result = "WIP" } + string getName() { result = modelName } + + string getType() { result = type } } - class BindingMode extends RequiredObject { - BindingMode() { this.getDependencyType() = "sap/ui/model/BindingMode" } + /** + * The definition of an external model in the `manifest.json`, in the `"models"` property. + */ + class ExternalModelManifest extends ModelManifest { + string modelName; + string dataSourceName; + + ExternalModelManifest() { + exists(JsonObject models | + this = models.getPropValue(modelName) and + dataSourceName = this.getPropStringValue("dataSource") and + /* This data source can be found in the "dataSources" property */ + exists(DataSourceManifest dataSource | dataSource.getName() = dataSourceName) + ) + } + + string getName() { result = modelName } - PropRead getOneWay() { result = this.getAPropertyRead("OneWay") } + string getDataSourceName() { result = dataSourceName } - PropRead getTwoWay() { result = this.getAPropertyRead("TwoWay") } + DataSourceManifest getDataSource() { result.getName() = dataSourceName } + } - PropRead getDefault() { result = this.getAPropertyRead("Default") } + class ManifestJson extends File { + string id; + + string getId() { result = id } + + ManifestJson() { + exists(JsonObject rootObj | + rootObj.getJsonFile() = this and + exists(string propertyName | exists(rootObj.getPropValue(propertyName)) | + propertyName = + [ + "sap.app", "sap.ui", "sap.ui5", "sap.platform.abap", "sap.platform.hcp", "sap.fiori", + "sap.card", "_version" + ] and + id = + rootObj.getPropValue("sap.app").(JsonObject).getPropValue("id").(JsonString).getValue() + ) + ) and + /* The name is fixed to "manifest.json": https://sapui5.hana.ondemand.com/sdk/#/topic/be0cf40f61184b358b5faedaec98b2da.html */ + this.getBaseName() = "manifest.json" + } - PropRead getOneTime() { result = this.getAPropertyRead("OneTime") } + DataSourceManifest getDataSource() { this = result.getManifestJson() } } +} - class RequiredObject extends SourceNode { - RequiredObject() { - exists(string dependencyType, int i | - any(SapDefineModule sapModule).getDependencyType(i) = dependencyType and - this = - any(SapDefineModule sapModule) - .getArgument(1) - .getALocalSource() - .(FunctionNode) - .getParameter(i) - ) - or - exists(string dependencyType | - this.toString() = dependencyType and - any(JQueryDefineModule jQueryModule).getADependencyType() = dependencyType +/** The manifest.json file serving as the app descriptor. */ +private string constructPathStringInner(Expr object) { + if not object instanceof ObjectExpr + then result = "" + else + exists(Property property | property = object.(ObjectExpr).getAProperty().(ValueProperty) | + result = "/" + property.getName() + constructPathStringInner(property.getInit()) + ) +} + +/** + * Create all recursive path strings of an object literal, e.g. + * if `object = { p1: { p2: 1 }, p3: 2 }`, then create: + * - `p1/p2`, and + * - `p3/`. + */ +private string constructPathString(DataFlow::ObjectLiteralNode object) { + result = constructPathStringInner(object.asExpr()) +} + +/** Holds if the `property` is in any way nested inside the `object`. */ +private predicate propertyNestedInObject(ObjectExpr object, Property property) { + exists(Property property2 | property2 = object.getAProperty() | + property = property2 or + propertyNestedInObject(property2.getInit().(ObjectExpr), property) + ) +} + +private string constructPathStringInner(Expr object, Property property) { + if not object instanceof ObjectExpr + then result = "" + else + exists(Property property2 | property2 = object.(ObjectExpr).getAProperty().(ValueProperty) | + if property = property2 + then result = "/" + property2.getName() + else ( + /* We're sure this property is inside this object */ + propertyNestedInObject(property2.getInit().(ObjectExpr), property) and + result = "/" + property2.getName() + constructPathStringInner(property2.getInit(), property) ) - } + ) +} + +/** + * Create all possible path strings of an object literal up to a certain property, e.g. + * if `object = { p1: { p2: 1 }, p3: 2 }` and `property = {p3: 2}` then create `"p3/"`. + */ +string constructPathString(DataFlow::ObjectLiteralNode object, Property property) { + result = constructPathStringInner(object.asExpr(), property) +} + +/** + * Create all recursive path strings of a JSON object, e.g. + * if `object = { "p1": { "p2": 1 }, "p3": 2 }`, then create: + * - `/p1/p2`, and + * - `/p3`. + */ +string constructPathStringJson(JsonValue object) { + if not object instanceof JsonObject + then result = "" + else + exists(string property | + result = "/" + property + constructPathStringJson(object.getPropValue(property)) + ) +} - UserModule getDefiningModule() { result.getArgument(1).(FunctionNode).getParameter(_) = this } +/** + * Create all possible path strings of a JSON object up to a certain property name, e.g. + * if `object = { "p1": { "p2": 1 }, "p3": 2 }` and `propName = "p3"` then create `"/p3"`. + * PRECONDITION: All of `object`'s keys are unique. + */ +bindingset[propName] +string constructPathStringJson(JsonValue object, string propName) { + exists(string pathString | pathString = constructPathStringJson(object) | + pathString.regexpMatch(".*" + propName + ".*") and + result = pathString + ) +} + +/** + * When given a constructor call `new JSONModel("controller/model.json")`, + * get the content of the file referred to by URI (`"controller/model.json"`) + * inside the string argument. + */ +bindingset[path] +JsonObject resolveDirectPath(string path) { + exists(WebApp webApp | result.getJsonFile() = webApp.getResource(path)) +} - string getDependencyType() { - exists(string dependencyType | - this.getDefiningModule().getRequiredObject(dependencyType) = this and - result = this.getDefiningModule().getADependencyType() +/** + * When given a constructor call `new JSONModel(sap.ui.require.toUrl("sap/ui/demo/mock/products.json")`, + * get the content of the file referred to by resolving the argument. + * Currently only supports `sap.ui.require.toUrl`. + */ +bindingset[path] +private JsonObject resolveIndirectPath(string path) { + result = any(JsonObject tODO | tODO.getFile().getAbsolutePath() = path) +} + +class JsonModel extends UI5InternalModel { + JsonModel() { + this instanceof NewNode and + ( + exists(RequiredObject jsonModel | + jsonModel.flowsTo(this.getCalleeNode()) and + jsonModel.getDependencyType() = "sap/ui/model/json/JSONModel" ) - } + or + /* Fallback */ + this.getCalleeName() = "JSONModel" + ) } /** - * `SomeModule.extend(...)` where `SomeModule` stands for a module imported with `sap.ui.define`. + * Gets all possible path strings that can be constructed from this JSON model. */ - class Extension extends InvokeNode, MethodCallNode { - Extension() { - /* 1. The receiver object is an imported one */ - any(RequiredObject module_).flowsTo(this.getReceiver()) and - /* 2. The method name is `extend` */ - this.(MethodCallNode).getMethodName() = "extend" - } + override string getPathString() { + /* 1. new JSONModel("controller/model.json") */ + if this.getAnArgument().asExpr() instanceof StringLiteral + then + result = + constructPathStringJson(resolveDirectPath(this.getAnArgument() + .asExpr() + .(StringLiteral) + .getValue())) + else + if this.getAnArgument().(MethodCallNode).getAnArgument().asExpr() instanceof StringLiteral + then + /* 2. new JSONModel(sap.ui.require.toUrl("sap/ui/demo/mock/products.json")) */ + result = + constructPathStringJson(resolveIndirectPath(this.getAnArgument() + .(MethodCallNode) + .getAnArgument() + .asExpr() + .(StringLiteral) + .getValue())) + else + /* + * 3. new JSONModel(oData) where + * var oData = { input: null }; + */ - string getName() { result = this.getArgument(0).asExpr().(StringLiteral).getValue() } + exists(ObjectLiteralNode objectNode | + objectNode.flowsTo(this.getAnArgument()) and constructPathString(objectNode) = result + ) + } - ObjectLiteralNode getContent() { result = this.getArgument(1) } + override string getPathString(Property property) { + /* + * 3. new JSONModel(oData) where + * var oData = { input: null }; + */ - Metadata getMetadata() { - result = this.getContent().getAPropertySource("metadata") - or - exists(Extension baseExtension | - baseExtension.getDefine().getExtendingDefine() = this.getDefine() and - result = baseExtension.getMetadata() - ) - } + exists(ObjectLiteralNode objectNode | + objectNode.flowsTo(this.getAnArgument()) and + constructPathString(objectNode, property) = result + ) + } - /** Gets the `sap.ui.define` call that wraps this extension. */ - SapDefineModule getDefine() { this.getEnclosingFunction() = result.getArgument(1).asExpr() } + bindingset[propName] + string getPathStringPropName(string propName) { + exists(JsonObject jsonObject | + jsonObject = + resolveDirectPath(this.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue()) + | + constructPathStringJson(jsonObject, propName) = result + ) } /** - * The property metadata found in an Extension. + * A model possibly supporting two-way binding explicitly set as a one-way binding model. */ - class Metadata extends ObjectLiteralNode { - CustomControl control; + predicate isOneWayBinding() { + exists(MethodCallNode call, BindingMode bindingMode | + this.flowsTo(call.getReceiver()) and + call.getMethodName() = "setDefaultBindingMode" and + bindingMode.getOneWay().flowsTo(call.getArgument(0)) + ) + } - CustomControl getControl() { result = control } + predicate isTwoWayBinding() { + // Either explicitly set as two-way, or + exists(MethodCallNode call, BindingMode bindingMode | + this.flowsTo(call.getReceiver()) and + call.getMethodName() = "setDefaultBindingMode" and + bindingMode.getTwoWay().flowsTo(call.getArgument(0)) + ) + or + // left untouched as default mode which is two-way. + not exists(MethodCallNode call | + this.flowsTo(call.getReceiver()) and + call.getMethodName() = "setDefaultBindingMode" + ) + } - Metadata() { this = control.getContent().getAPropertySource("metadata") } + /** + * Get a property of this `JsonModel`, e.g. given a JSON model `oModel` defined either of the following: + * ```javascript + * oModel = new JSONModel({x: null}); + * ``` + * ```javascript + * oContent = {x: null}; + * oModel = new JSONModel(oContent); + * ``` + * Get `x: null` as its result. + */ + DataFlow::PropWrite getAProperty() { + this.getArgument(0).getALocalSource().asExpr() = result.getPropertyNameExpr().getParent+() + } +} - SourceNode getProperty(string name) { - result = this.getAPropertySource("properties").getAPropertySource(name) - } +class XmlModel extends UI5InternalModel { + XmlModel() { + this instanceof NewNode and + exists(RequiredObject xmlModel | + xmlModel.flowsTo(this.getCalleeNode()) and + xmlModel.getDependencyType() = "sap/ui/model/xml/XMLModel" + ) + } - predicate isUnrestrictedStringType(string propName) { - /* text : "string" */ - exists(SourceNode propRef | - propRef = this.getProperty(propName) and - propRef.asExpr().(StringLiteral).getValue() = "string" - ) - or - /* text: { type: "string" } */ - exists(SourceNode propRef | - propRef = this.getProperty(propName) and - propRef.getAPropertySource("type").asExpr().(StringLiteral).getValue() = "string" - ) - or - /* text: { someOther: "someOtherVal", ... } */ - exists(SourceNode propRef | - propRef = this.getProperty(propName) and - not exists(propRef.getAPropertySource("type")) + override string getPathString(Property property) { + /* TODO */ + result = property.toString() + } + + override string getPathString() { result = "TODO" } +} + +class BindingMode extends RequiredObject { + BindingMode() { this.getDependencyType() = "sap/ui/model/BindingMode" } + + PropRead getOneWay() { result = this.getAPropertyRead("OneWay") } + + PropRead getTwoWay() { result = this.getAPropertyRead("TwoWay") } + + PropRead getDefault() { result = this.getAPropertyRead("Default") } + + PropRead getOneTime() { result = this.getAPropertyRead("OneTime") } +} + +class RequiredObject extends SourceNode { + RequiredObject() { + exists(SapDefineModule sapDefineModule | + this = sapDefineModule.getArgument(1).getALocalSource().(FunctionNode).getParameter(_) + ) or + exists(JQueryDefineModule jQueryDefineModule | + this.toString() = + jQueryDefineModule.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() + ) + } + + UserModule getDefiningModule() { result.getArgument(1).(FunctionNode).getParameter(_) = this } + + string getDependencyType() { + exists(SapDefineModule module_ | this = module_.getRequiredObject(result)) + } +} + +/** + * `SomeModule.extend(...)` where `SomeModule` stands for a module imported with `sap.ui.define`. + */ +class Extension extends InvokeNode, MethodCallNode { + Extension() { + /* 1. The receiver object is an imported one */ + any(RequiredObject module_).flowsTo(this.getReceiver()) and + /* 2. The method name is `extend` */ + this.(MethodCallNode).getMethodName() = "extend" + } + + FunctionNode getAMethod() { + result = this.getArgument(1).(ObjectLiteralNode).getAPropertySource().(FunctionNode) + } + + string getName() { result = this.getArgument(0).asExpr().(StringLiteral).getValue() } + + ObjectLiteralNode getContent() { result = this.getArgument(1) } + + Metadata getMetadata() { + result = this.getContent().getAPropertySource("metadata") + or + exists(Extension baseExtension | + baseExtension.getDefine().getExtendingDefine() = this.getDefine() and + result = baseExtension.getMetadata() + ) + } + + /** Gets the `sap.ui.define` call that wraps this extension. */ + SapDefineModule getDefine() { this.getEnclosingFunction() = result.getArgument(1).asExpr() } +} + +newtype TSapElement = + DefinitionOfElement(Extension extension) or + ReferenceOfElement(Reference reference) + +class SapElement extends TSapElement { + Extension asDefinition() { this = DefinitionOfElement(result) } + + Reference asReference() { this = ReferenceOfElement(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() + } + + string toString() { + result = this.asDefinition().toString() or + result = this.asReference().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) + } +} + +/** + * The property metadata found in an Extension. + */ +class Metadata extends ObjectLiteralNode { + Extension extension; + + Extension getExtension() { result = extension } + + Metadata() { this = extension.getContent().getAPropertySource("metadata") } + + SourceNode getProperty(string name) { + result = + any(PropertyMetadata property | + property.getParentMetadata() = this and property.getName() = name ) - } + } +} + +class AggregationMetadata extends ObjectLiteralNode { + string name; + Metadata parentMetadata; + + AggregationMetadata() { + this = parentMetadata.getAPropertySource("aggregations").getAPropertySource(name) + } + + Metadata getParentMetadata() { result = parentMetadata } + + string getName() { result = name } + + /** + * Gets the type of this aggregation. + */ + string getType() { + result = this.getAPropertySource("type").getALocalSource().asExpr().(StringLiteral).getValue() + } +} + +class PropertyMetadata extends ObjectLiteralNode { + string name; + Metadata parentMetadata; + + PropertyMetadata() { + this = parentMetadata.getAPropertySource("properties").getAPropertySource(name) + } - bindingset[propName] - MethodCallNode getAWrite(string propName) { + Metadata getParentMetadata() { result = parentMetadata } + + string getName() { result = name } + + /** + * Gets the type of this aggregation. + */ + string getType() { + if this.isUnrestrictedStringType() + then result = "string" + else + result = this.getAPropertySource("type").getALocalSource().asExpr().(StringLiteral).getValue() + } + + /** + * Holds if this property's type is an unrestricted string not belonging to any enum. + * This makes the property a possible avenue of a client-side XSS. + */ + predicate isUnrestrictedStringType() { + /* text : "string" */ + this.asExpr().(StringLiteral).getValue() = "string" + or + /* text: { type: "string" } */ + this.getAPropertySource("type").asExpr().(StringLiteral).getValue() = "string" + or + /* text: { someOther: "someOtherVal", ... } */ + not exists(this.getAPropertySource("type")) + } + + MethodCallNode getAWrite() { + ( + result.getMethodName() = "set" + capitalize(name) + or result.getMethodName() = "setProperty" and - result.getArgument(0).asExpr().(StringLiteral).getValue() = propName and - // TODO: in same controller - exists(WebApp webApp | - webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() - ) - } + result.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() = name + ) and + exists(WebApp webApp | + webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() + ) + } - bindingset[propName] - MethodCallNode getARead(string propName) { - result.getMethodName() = "get" + capitalize(propName) and - // TODO: in same controller - exists(WebApp webApp | - webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() - ) - } + MethodCallNode getARead() { + ( + result.getMethodName() = "get" + capitalize(name) + or + result.getMethodName() = "getProperty" and + result.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() = name + ) and + exists(WebApp webApp | + webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() + ) } } diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5DataFlow.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5DataFlow.qll deleted file mode 100644 index 2e7a910d..00000000 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5DataFlow.qll +++ /dev/null @@ -1,265 +0,0 @@ -import javascript -import advanced_security.javascript.frameworks.ui5.UI5::UI5 -import advanced_security.javascript.frameworks.ui5.UI5View -import advanced_security.javascript.frameworks.ui5.UI5AMDModule -private import DataFlow::PathGraph as DataFlowPathGraph - -module UI5DataFlow { - /** - * Additional Flow Step: - * Binding path in the model <-> control metadata - */ - private predicate bidiModelControl(DataFlow::Node start, DataFlow::Node end) { - exists(DataFlow::SourceNode property, Metadata metadata, UI5BoundNode node | - // same project - exists(WebApp webApp | - webApp.getAResource() = metadata.getFile() and webApp.getAResource() = node.getFile() - ) and - ( - // same control - metadata.getControl().getName() = node.getBindingPath().getControlQualifiedType() - or - // extended control - exists(Extension subclass | - metadata.getControl().getDefine().getExtendingDefine() = subclass.getDefine() and - node.getBindingPath().getControlQualifiedType() = subclass.getName() - ) - ) and - property = metadata.getProperty(node.getBindingPath().getPropertyName()) and - ( - start = property and end = node - or - start = node and end = property - ) - ) - } - - predicate isAdditionalFlowStep( - DataFlow::Node start, DataFlow::Node end, DataFlow::FlowLabel inLabel, - DataFlow::FlowLabel outLabel - ) { - inLabel = "taint" and - outLabel = "taint" and - ( - bidiModelControl(start, end) - or - // handler argument node to handler parameter - exists(UI5Handler h | - start = h.getBindingPath().getNode() and - // ideally we would like to show an intermediate node where - // the handler is bound to a control, but there is no sourceNode there - // `end = h.getBindingPath() or start = h.getBindingPath()` - end = h.getParameter(0) - ) - or - /* 1. Control metadata property being the intermediate flow node */ - exists(string propName, Metadata metadata | - // writing site -> control metadata - start = metadata.getAWrite(propName).getArgument(1) and - end = metadata.getProperty(propName) - or - // control metadata -> reading site - start = metadata.getProperty(propName) and - end = metadata.getARead(propName) - ) - or - /* 2. Model property being the intermediate flow node */ - // JS object property (corresponding to binding path) -> getProperty('/path') - start = end.(GetBoundValue).getBind() - or - // setProperty('/path') -> JS object property (corresponding to binding path) - end = start.(SetBoundValue).getBind() - // or - /* 3. Argument to JSONModel constructor being the intermediate flow node */ - // exists(UI5 model, GetBoundValue getP | - // start = getP and - // model.getPathString() = getP.getArgument(0).asExpr().(StringLiteral).getValue() and - // end = model.(JsonModel).getAnArgument() and - // end.asExpr() instanceof StringLiteral - // ) - ) - } - - /** - * Models dataflow nodes bound to a UI5 View via binding path - */ - class UI5BoundNode extends DataFlow::Node { - UI5BindingPath bindingPath; - - UI5BindingPath getBindingPath() { result = bindingPath } - - UI5BoundNode() { - exists(WebApp webApp | - webApp.getAResource() = this.getFile() and - webApp.getAResource() = bindingPath.getFile() - | - /* The relevant portion of the content of a JSONModel */ - exists(Property p, JsonModel model | - // The property bound to an UI5View source - this.(DataFlow::PropRef).getPropertyNameExpr() = p.getNameExpr() and - // The binding path refers to this model - bindingPath.getAbsolutePath() = model.getPathString(p) - ) - or - /* The URI string to the JSONModel constructor call */ - exists(JsonModel model | - this = model.getArgument(0) and - this.asExpr() instanceof StringLiteral and - bindingPath.getAbsolutePath() = model.getPathString() - ) - ) - } - } - - /** - * An remote source associated with a `UI5BoundNode` - */ - class UI5ModelSource extends UI5DataFlow::UI5BoundNode, RemoteFlowSource { - UI5ModelSource() { bindingPath = any(UI5View view).getASource() } - - override string getSourceType() { result = "UI5 model remote flow source" } - } - - /** - * An html injection sink associated with a `UI5BoundNode` - */ - class UI5ModelHtmlISink extends UI5DataFlow::UI5BoundNode { - UI5View view; - - UI5ModelHtmlISink() { - not view.getController().getModel().(JsonModel).isOneWayBinding() and - bindingPath = view.getAnHtmlISink() - } - } - - /** - * Models calls to `Model.getProperty` and `Model.getObject` - */ - class GetBoundValue extends DataFlow::MethodCallNode { - UI5BoundNode bind; - - GetBoundValue() { - // direct read access to a binding path - this.getCalleeName() = ["getProperty", "getObject"] and - bind.getBindingPath().getAbsolutePath() = this.getArgument(0).getStringValue() and - exists(DataFlow::SourceNode receiverSource, UI5Model model | - receiverSource = this.getReceiver().getALocalSource() and - model = bind.getBindingPath().getModel() - | - model = receiverSource - or - model.getController().getAModelReference() = receiverSource - ) - } - - UI5BoundNode getBind() { result = bind } - } - - /** - * Models calls to `Model.setProperty` and `Model.setObject` - */ - class SetBoundValue extends DataFlow::Node { - UI5BoundNode bind; - - SetBoundValue() { - exists(DataFlow::MethodCallNode setProp | - // direct access to a binding path - this = setProp.getArgument(1) and - setProp.getCalleeName() = ["setProperty", "setObject"] and - bind.getBindingPath().getAbsolutePath() = setProp.getArgument(0).getStringValue() and - exists(DataFlow::SourceNode receiverSource, UI5Model model | - receiverSource = setProp.getReceiver().getALocalSource() - | - model = bind.getBindingPath().getModel() and - ( - model = receiverSource - or - model.getController().getAModelReference() = receiverSource - ) - ) - ) - } - - UI5BoundNode getBind() { result = bind } - } -} - -module UI5PathGraph { - newtype TNode = - TUI5BindingPathNode(UI5BindingPath path) or - TDataFlowPathNode(DataFlow::Node node) - - class UI5PathNode extends TNode { - DataFlow::PathNode asDataFlowPathNode() { this = TDataFlowPathNode(result.getNode()) } - - UI5BindingPath asUI5BindingPathNode() { this = TUI5BindingPathNode(result) } - - string toString() { - result = this.asDataFlowPathNode().toString() - or - result = this.asUI5BindingPathNode().toString() - } - - predicate hasLocationInfo( - string filepath, int startline, int startcolumn, int endline, int endcolumn - ) { - this.asDataFlowPathNode() - .getNode() - .hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) - or - this.asUI5BindingPathNode() - .getLocation() - .hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn) - } - - UI5PathNode getAPrimarySource() { - not this.asDataFlowPathNode().getNode() instanceof UI5DataFlow::UI5BoundNode and - this.asDataFlowPathNode() = result.asDataFlowPathNode() - or - this.asDataFlowPathNode().getNode().(UI5DataFlow::UI5BoundNode).getBindingPath() = - result.asUI5BindingPathNode() and - result.asUI5BindingPathNode() = any(UI5View view).getASource() - } - - UI5PathNode getAPrimaryHtmlISink() { - not this.asDataFlowPathNode().getNode() instanceof UI5DataFlow::UI5BoundNode and - this.asDataFlowPathNode() = result.asDataFlowPathNode() - or - this.asDataFlowPathNode().getNode().(UI5DataFlow::UI5BoundNode).getBindingPath() = - result.asUI5BindingPathNode() and - result.asUI5BindingPathNode() = any(UI5View view).getAnHtmlISink() - } - } - - query predicate nodes(UI5PathNode nd) { - exists(nd.asUI5BindingPathNode()) - or - DataFlowPathGraph::nodes(nd.asDataFlowPathNode()) - } - - query predicate edges(UI5PathNode pred, UI5PathNode succ) { - // all dataflow edges - DataFlowPathGraph::edges(pred.asDataFlowPathNode(), succ.asDataFlowPathNode()) and - // exclude duplicate edge from model to handler parameter - not exists(UI5Handler h | - pred.asDataFlowPathNode().getNode() = h.getBindingPath().getNode() and - succ.asDataFlowPathNode().getNode() = h.getParameter(0) - ) - or - pred.asUI5BindingPathNode() = - succ.asDataFlowPathNode().getNode().(UI5DataFlow::UI5BoundNode).getBindingPath() and - pred.asUI5BindingPathNode() = any(UI5View view).getASource() - or - succ.asUI5BindingPathNode() = - pred.asDataFlowPathNode().getNode().(UI5DataFlow::UI5BoundNode).getBindingPath() and - succ.asUI5BindingPathNode() = any(UI5View view).getAnHtmlISink() - or - // flow to event handler parameter through the binding argument - pred.asDataFlowPathNode().getNode() = succ.asUI5BindingPathNode().getNode() - or - exists(UI5Handler h | - pred.asUI5BindingPathNode() = h.getBindingPath() and - succ.asDataFlowPathNode().getNode() = h.getParameter(0) - ) - } -} diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5HTML.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5HTML.qll index 96d9f130..02e3b3e2 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5HTML.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5HTML.qll @@ -1,6 +1,6 @@ -private import javascript -private import DataFlow -private import advanced_security.javascript.frameworks.ui5.UI5 +import javascript +import DataFlow +import advanced_security.javascript.frameworks.ui5.UI5 newtype TFrameOptions = /* @@ -87,7 +87,7 @@ class FrameOptions extends TFrameOptions { /** * Holds if there are no frame options specified to prevent click jacking. */ -predicate isMissingFrameOptionsToPreventClickjacking(UI5::WebApp webapp) { +predicate isMissingFrameOptionsToPreventClickjacking(WebApp webapp) { not exists(FrameOptions frameOptions | webapp.getFrameOptions() = frameOptions | frameOptions.allowsSharedOriginEmbedding() or frameOptions.deniesEmbedding() or diff --git a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5View.qll b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5View.qll index 664c3a3b..2a5c65d7 100644 --- a/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5View.qll +++ b/javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5View.qll @@ -1,190 +1,272 @@ -private import javascript -private import DataFlow -private import advanced_security.javascript.frameworks.ui5.UI5::UI5 +import javascript +import DataFlow +import advanced_security.javascript.frameworks.ui5.UI5 +import advanced_security.javascript.frameworks.ui5.dataflow.DataFlow private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions +import advanced_security.javascript.frameworks.ui5.Bindings /** - * Utiility predicate returning - * types that are supertype of the argument - * ``` - * data:["sap/m/InputBase", "sap/m/Input", ""] - * ``` - */ -bindingset[base] -private string getASuperType(string base) { - result = base or ApiGraphModelsExtensions::typeModel(result, base, "") -} - -/** - * Utility predicate capturing - * the binding path in the argument - * ``` - * value: "{Control>country}" + * Gets the immediate supertype of a given type from the extensible predicate `typeModel` provided by + * Model-as-Data Extension to the CodeQL runtime. If no type is defined as a supertype of a given one, + * then this predicate is reflexive. e.g. + * If there is a row such as below in the extension file: + * ```yaml + * ["sap/m/InputBase", "sap/m/Input", ""] * ``` + * Then it gets `"sap/m/InputBase"` when given `"sap/m/Input"`. However, if no such row is present, then + * this predicate simply binds `result` to the given `"sap/m/Input"`. + * + * This predicate is good for modeling the object-oriented class hierarchy in UI5. */ -bindingset[property] -private string bindingPathCapture(string property) { - exists(string pattern | - // matches "Control>country" - pattern = "(?:[^'\"\\}]+>)?([^'\"\\}]*)" and - ( - // simple {Control>country} - result = property.replaceAll(" ", "").regexpCapture("(?s)\\{" + pattern + "\\}", 1) - or - // object {other:{foo:'bar'} path: 'Result>country'} - result = - property - .replaceAll(" ", "") - .regexpCapture("(?s)\\{[^\"]*path:'" + pattern + "'[^\"]*\\}", 1) - or - // event handler simple parameter {.doSomething(${/input})} - result = - property - .replaceAll(" ", "") - .regexpCapture("(?s)\\.[\\w-]+\\(\\$\\{" + pattern + "\\}\\)", 1) - ) - ) +bindingset[type] +string getASuperType(string type) { + result = type or ApiGraphModelsExtensions::typeModel(result, type, "") } /** - * Models a binding path - * like the property value `{/input}` in the following example - * ``` - * { - * "Type": "sap.m.Input", - * "value": "{/input}" - * } - * ``` + * A [binding path](https://sapui5.hana.ondemand.com/sdk/#/topic/2888af49635949eca14fa326d04833b9) that refers + * to a piece of data in a model, whether it is internal (client-side) or external (server-side). It is found in a file which defines a view declaratively, using either XML, + * HTML, JSON or JavaScript, and is a property of an XML/HTML element or a JSON/JavaScript object. + * + * Since these data cannot be recognized as `DataFlow::Node`s (with an exception of JS objects), a `UI5BindingPath` + * is always represented by a `UI5BoundNode` to which this `UI5BindingPath` refers to. */ -abstract class UI5BindingPath extends Locatable { +abstract class UI5BindingPath extends BindingPath { /** - * Return the value of the binding path - * as specified in the view + * Gets the string value of this path, without the surrounding curly braces. */ abstract string getPath(); /** - * Return the absolute value of the binding path + * Gets the string value of this path with the surrounding curly braces. + */ + abstract string getLiteralRepr(); + + /** + * Resolve this path to an absolute one. It gets itself for an already absolute path. */ abstract string getAbsolutePath(); /** - * Return the name of the property associated to a binding path + * Gets the name of the associated control. */ abstract string getPropertyName(); /** - * Return the qualified type of the associated Control + * Gets the fully qualified type of the associated control. */ abstract string getControlQualifiedType(); /** - * Return the name of the associated Control + * Gets the full import path of the associated control. */ string getControlTypeName() { result = this.getControlQualifiedType().replaceAll(".", "/") } /** - * Get the model declaration, which this data binding refers to in a Controller + * Gets the view that this binding path resides in. + */ + UI5View getView() { + /* 1. Declarative, inside a certain data format. */ + this.getLocation().getFile() = result + or + /* 2. Procedural, inside a body of a controller handler. */ + exists(CustomController controller | + controller.getFile() = this.getLocation().getFile() and + controller.getView() = result + ) + } + + /** + * Gets the UI5Control using this UI5BindingPath. + */ + abstract UI5Control getControlDeclaration(); + + /** + * Gets the model, attached to either a control or a view, that this binding path refers to. */ UI5Model getModel() { - exists(UI5View view | - this.getFile() = view and - view.getController().getModel() = result + ( + /* 1. The result is a named model and the names in the controlSetModel call and in the binding path match up, but the viewSetModelCall isn't the case. */ + exists(MethodCallNode controlSetModelCall | + controlSetModelCall.getMethodName() = "setModel" and + this.getControlDeclaration().getAReference().flowsTo(controlSetModelCall.getReceiver()) and + controlSetModelCall.getArgument(1).getALocalSource().asExpr().(StringLiteral).getValue() = + this.getModelName() and + result.flowsTo(controlSetModelCall.getArgument(0)) + ) + or + /* 2. The result is a default (nameless) model and both the controlSetModel call and the binding path lack a model name, but the viewSetModeCall isn't the case. */ + exists(MethodCallNode controlSetModelCall | + controlSetModelCall.getMethodName() = "setModel" and + this.getControlDeclaration().getAReference().flowsTo(controlSetModelCall.getReceiver()) and + not exists(controlSetModelCall.getArgument(1)) and + not exists(this.getModelName()) and + result.flowsTo(controlSetModelCall.getArgument(0)) + ) + or + /* 3. There is no call to `setModel` on a control reference that sets a named model, so we look if the view reference has one. */ + exists(MethodCallNode viewSetModelCall | + viewSetModelCall.getMethodName() = "setModel" and + this.getView().getController().getAViewReference().flowsTo(viewSetModelCall.getReceiver()) and + viewSetModelCall.getArgument(1).getALocalSource().asExpr().(StringLiteral).getValue() = + this.getModelName() and + result.flowsTo(viewSetModelCall.getArgument(0)) + ) and + not exists(MethodCallNode controlSetModelCall | + controlSetModelCall.getMethodName() = "setModel" and + this.getControlDeclaration().getAReference().flowsTo(controlSetModelCall.getReceiver()) and + controlSetModelCall.getArgument(1).getALocalSource().asExpr().(StringLiteral).getValue() = + this.getModelName() + ) + or + /* 4. There is no call to `setModel` on a control reference that set an unnamed model, so we look if the view reference has one. */ + exists(MethodCallNode viewSetModelCall | + viewSetModelCall.getMethodName() = "setModel" and + this.getView().getController().getAViewReference().flowsTo(viewSetModelCall.getReceiver()) and + not exists(viewSetModelCall.getArgument(1)) and + not exists(this.getModelName()) and + result.flowsTo(viewSetModelCall.getArgument(0)) + ) and + not exists(MethodCallNode controlSetModelCall | + controlSetModelCall.getMethodName() = "setModel" and + this.getControlDeclaration().getAReference().flowsTo(controlSetModelCall.getReceiver()) and + not exists(controlSetModelCall.getArgument(1)) and + not exists(this.getModelName()) + ) ) + // and + // /* This binding path and the resulting model should live inside the same webapp */ + // exists(WebApp webApp | + // webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() + // ) } - DataFlow::PropWrite getNode() { + /** + * Gets the `DataFlow::Node` that represents this binding path. + */ + Node getNode() { + /* 1-1. Internal (Client-side) model, model hardcoded in JS code */ exists(Property p, JsonModel model | - // The property bound to an UI5View source - result.getPropertyNameExpr() = p.getNameExpr() and + /* Get the property of an JS object bound to this binding path. */ + result.(DataFlow::PropWrite).getPropertyNameExpr() = p.getNameExpr() and this.getAbsolutePath() = model.getPathString(p) and - //restrict search inside the same webapp + /* Restrict search to inside the same webapp. */ exists(WebApp webApp | - webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() + webApp.getAResource() = this.getLocation().getFile() and + webApp.getAResource() = result.getFile() ) ) - // TODO - /* - * or exists(string propName, JsonModel model | ... - * model.getPathStringPropName(propName) - * ) - */ - - } -} - -abstract class UI5ControlProperty extends Locatable { - abstract UI5Control getControl(); - - abstract string getName(); - - abstract string getValue(); + or + /* 1-2. Internal (Client-side) model, model loaded from JSON file */ + exists(string propName, JsonModel model | + /* Get the property of an JS object bound to this binding path. */ + result = model.getArgument(0).getALocalSource() and + this.getPath() = model.getPathStringPropName(propName) and + exists(JsonObject obj, JsonValue val | val = obj.getPropValue(propName)) + ) + or + /* 2. External (Server-side) model */ + result = this.getModel().(UI5ExternalModel) + } } -class XmlControlProperty extends UI5ControlProperty instanceof XmlAttribute { - XmlControlProperty() { this.getElement() = any(XmlControl control) } +class XmlControlProperty extends XmlAttribute { + XmlControlProperty() { exists(UI5Control control | this.getElement() = control.asXmlControl()) } override string getName() { result = XmlAttribute.super.getName() } override string getValue() { result = XmlAttribute.super.getValue() } +} - override UI5Control getControl() { result = XmlAttribute.super.getElement() } +bindingset[qualifiedTypeUri] +predicate isBuiltInControl(string qualifiedTypeUri) { + exists(string namespace | + namespace = + [ + "sap\\.m.*", // https://sapui5.hana.ondemand.com/#/api/sap.m: The main UI5 control library, with responsive controls that can be used in touch devices as well as desktop browsers. + "sap\\.f.*", // https://sapui5.hana.ondemand.com/#/api/sap.f: SAPUI5 library with controls specialized for SAP Fiori apps. + "sap\\.ui.*" // https://sapui5.hana.ondemand.com/#/api/sap.ui: The sap.ui namespace is the central OpenAjax compliant entry point for UI related JavaScript functionality provided by SAP. + ] + | + qualifiedTypeUri.regexpMatch(namespace) + ) } /** - * Models a UI5 View that might include - * XSS sources and sinks in standard controls + * A UI5 View that might include XSS sources and sinks in standard controls. */ +/* TODO: Update docstring */ abstract class UI5View extends File { abstract string getControllerName(); /** - * Get the Controller.extends(...) definition associated with this View. + * Get the `Controller.extends(...)` definition associated with this View. */ CustomController getController() { - // The controller name should match + /* The controller name should match between the view and the controller definition. */ result.getName() = this.getControllerName() and - // The View and the Controller are in a same webapp + /* The View and the Controller are in a same webapp. */ exists(WebApp webApp | webApp.getAResource() = this and webApp.getAResource() = result.getFile() ) } + abstract UI5Control getControl(); + abstract UI5BindingPath getASource(); abstract UI5BindingPath getAnHtmlISink(); } -class JsonBindingPath extends UI5BindingPath, JsonValue { - string path; +JsonBindingPath getJsonItemsBinding(JsonBindingPath bindingPath) { + exists(Binding itemsBinding | + itemsBinding.getBindingTarget().asJsonObjectProperty("items") = + bindingPath.getBindingTarget().getParent+() and + result = itemsBinding.getBindingPath() and + result != bindingPath // exclude ourselves + ) and + not exists(bindingPath.getModelName()) +} + +/** + * A UI5BindingPath found in a JSON View. + */ +class JsonBindingPath extends UI5BindingPath { + string boundPropertyName; + Binding binding; + JsonObject bindingTarget; - JsonBindingPath() { path = bindingPathCapture(this.getStringValue()) } + JsonBindingPath() { + bindingTarget = binding.getBindingTarget().asJsonObjectProperty(boundPropertyName) and + binding.getBindingPath() = this + } override string toString() { - result = "\"" + this.getPropertyName() + "\": \"" + this.getStringValue() + "\"" + result = + "\"" + boundPropertyName + "\": \"" + bindingTarget.getPropStringValue(boundPropertyName) + + "\"" } - override string getPath() { result = path } + override string getLiteralRepr() { result = bindingTarget.getPropStringValue(boundPropertyName) } + + override string getPath() { result = this.asString() } override string getAbsolutePath() { - if path.matches("/%") - then result = path + if this.isAbsolute() + then result = this.asString() else - exists(JsonBindingPath composite_path | - composite_path != this and - composite_path = this.getParent+().(JsonObject).getPropValue("items") and - result = composite_path.getAbsolutePath() + "/" + path - ) + if exists(getJsonItemsBinding(this)) + then result = getJsonItemsBinding(this).getAbsolutePath() + "/" + this.asString() + else result = this.asString() } - override string getPropertyName() { this = any(JsonValue v).getPropValue(result) } + override string getPropertyName() { result = boundPropertyName } - override string getControlQualifiedType() { - exists(JsonObject control | - this = control.getPropValue(this.getPropertyName()) and - result = control.getPropStringValue("Type") - ) - } + override string getControlQualifiedType() { result = bindingTarget.getPropStringValue("Type") } + + JsonObject getBindingTarget() { result = bindingTarget } + + override UI5Control getControlDeclaration() { result.asJsonControl() = bindingTarget } } class JsView extends UI5View { @@ -202,6 +284,29 @@ class JsView extends UI5View { ) } + MethodCallNode getRoot() { result = rootJsViewCall } + + override UI5Control getControl() { + exists(NewNode node | + result.asJsControl() = node and + /* Use getAChild+ because some controls nest other controls inside them as aggregations */ + node.asExpr() = rootJsViewCall.asExpr().getAChild+() and + ( + /* 1. A builtin control provided by UI5 */ + isBuiltInControl(node.asExpr().getAChildExpr().(DotExpr).getQualifiedName()) + or + /* 2. A custom control with implementation code found in the webapp */ + exists(CustomControl control | + control.getName() = node.asExpr().getAChildExpr().(DotExpr).getQualifiedName() and + exists(WebApp webApp | + webApp.getAResource() = control.getFile() and + webApp.getAResource() = node.getFile() + ) + ) + ) + ) + } + override string getControllerName() { exists(FunctionNode function | function = @@ -214,23 +319,23 @@ class JsView extends UI5View { ) } - override JsBindingPath getASource() { - exists(ObjectExpr control, string type, string path, string property | + override JsViewBindingPath getASource() { + exists(DataFlow::ObjectLiteralNode control, string type, string path, string property | this = control.getFile() and type = result.getControlTypeName() and ApiGraphModelsExtensions::sourceModel(getASuperType(type), path, "remote") and property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and - result = control.getPropertyByName(property) + result.getBinding().getBindingTarget().asDataFlowNode() = control.getAPropertyWrite(property) ) } - override JsBindingPath getAnHtmlISink() { - exists(ObjectExpr control, string type, string path, string property | + override JsViewBindingPath getAnHtmlISink() { + exists(DataFlow::ObjectLiteralNode control, string type, string path, string property | this = control.getFile() and type = result.getControlTypeName() and ApiGraphModelsExtensions::sinkModel(getASuperType(type), path, "ui5-html-injection") and property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and - result = control.getPropertyByName(property) + result.getBinding().getBindingTarget().asDataFlowNode() = control.getAPropertyWrite(property) ) } } @@ -243,6 +348,28 @@ class JsonView extends UI5View { this = root.getJsonFile() } + JsonObject getRoot() { result = root } + + override UI5Control getControl() { + exists(JsonObject object | + root = result.asJsonControl().getParent+() and + /* Use getAChild+ because some controls nest other controls inside them as aggregations */ + ( + /* 1. A builtin control provided by UI5 */ + isBuiltInControl(object.getPropStringValue("Type")) + or + /* 2. A custom control with implementation code found in the webapp */ + exists(CustomControl control | + control.getName() = object.getPropStringValue("Type") and + exists(WebApp webApp | + webApp.getAResource() = control.getFile() and + webApp.getAResource() = object.getFile() + ) + ) + ) + ) + } + override string getControllerName() { result = root.getPropStringValue("controllerName") } override JsonBindingPath getASource() { @@ -251,7 +378,7 @@ class JsonView extends UI5View { type = result.getControlTypeName() and ApiGraphModelsExtensions::sourceModel(getASuperType(type), path, "remote") and property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and - result = control.getPropValue(property) + result.getBindingTarget() = control ) } @@ -261,71 +388,102 @@ class JsonView extends UI5View { type = result.getControlTypeName() and ApiGraphModelsExtensions::sinkModel(getASuperType(type), path, "ui5-html-injection") and property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and - result = control.getPropValue(property) + result.getBindingTarget() = control ) } } -class JsBindingPath extends UI5BindingPath, Property { - string path; +class JsViewBindingPath extends UI5BindingPath { + DataFlow::PropWrite bindingTarget; + Binding binding; - JsBindingPath() { - path = bindingPathCapture(this.getInit().getStringValue()) and - this.(Property).getFile() instanceof JsView + JsViewBindingPath() { + bindingTarget = binding.getBindingTarget().asDataFlowNode() and + binding.getBindingPath() = this } - private string dotExprToStringInner(Expr expr) { - if not expr instanceof DotExpr - then result = expr.toString() - else - exists(Expr subexpr, string propName | - expr.(DotExpr).accesses(subexpr, propName) and - result = dotExprToStringInner(subexpr) + "." + propName - ) - } - - /** `a.b.c.d.e.f.g(...)` => `"a.b.c.d.e.f.g"` */ - private string dotExprToString(DotExpr dot) { result = dotExprToStringInner(dot) } + override string getLiteralRepr() { result = bindingTarget.getALocalSource().getStringValue() } /* `new sap.m.Input({...})` => `"sap.m.Input"` */ override string getControlQualifiedType() { result = - dotExprToString(this.getInit().(StringLiteral).getParent+().(NewExpr).getCallee().(DotExpr)) + bindingTarget + .getPropertyNameExpr() + .getParent+() + .(NewExpr) + .getAChildExpr() + .(DotExpr) + .getQualifiedName() + } + + override string getAbsolutePath() { + /* TODO: Implement this properly! */ + result = this.getPath() } - override string getAbsolutePath() { result = path /* ??? */ } + override string getPath() { result = this.asString() } - override string getPath() { result = path } + override string getPropertyName() { + exists(DataFlow::ObjectLiteralNode initializer | + initializer.getAPropertyWrite(result).getRhs() = bindingTarget + ) + } - override string getPropertyName() { result = this.getName() } + override UI5Control getControlDeclaration() { + result.asJsControl().asExpr() = bindingTarget.getPropertyNameExpr().getParentExpr+().(NewExpr) + } } -class HtmlBindingPath extends UI5BindingPath, HTML::Attribute { - string path; +HtmlBindingPath getHtmlItemsBinding(HtmlBindingPath bindingPath) { + exists(Binding itemsBinding | + result != bindingPath and + itemsBinding.getBindingTarget().asXmlAttribute().getName() = "items" and + bindingPath.getBindingTarget().getElement().getParent+().(HTML::Element).getAnAttribute() = + itemsBinding.getBindingTarget().asXmlAttribute() and + result = itemsBinding.getBindingPath() + ) and + not exists(bindingPath.getModelName()) +} - HtmlBindingPath() { path = bindingPathCapture(this.getValue()) } +/** + * A UI5BindingPath found in an HTML View. + */ +class HtmlBindingPath extends UI5BindingPath { + HTML::Attribute bindingTarget; + Binding binding; - override string getPath() { result = path } + HtmlBindingPath() { + bindingTarget = binding.getBindingTarget().asXmlAttribute() and + binding.getBindingPath() = this + } + + override string getPath() { result = this.asString() } + + override string getLiteralRepr() { result = bindingTarget.getValue() } override string getAbsolutePath() { - if path.matches("/%") - then result = path + if this.isAbsolute() + then result = this.asString() else - exists(HtmlBindingPath composite_path | - composite_path != this and - composite_path = this.getElement().getParent+().(HTML::Element).getAttributeByName("items") and - result = composite_path.getAbsolutePath() + "/" + path - ) + if exists(getHtmlItemsBinding(this)) + then result = getHtmlItemsBinding(this).getPath() + "/" + this.getPath() + else result = this.asString() } - override string getPropertyName() { this = any(HTML::Element v).getAttributeByName(result) } + override string getPropertyName() { result = bindingTarget.getName() } override string getControlQualifiedType() { exists(HTML::Element control | - this = control.getAttributeByName(this.getPropertyName()) and + bindingTarget = control.getAttributeByName(this.getPropertyName()) and result = control.getAttributeByName("data-sap-ui-type").getValue() ) } + + HTML::Attribute getBindingTarget() { result = bindingTarget } + + override UI5Control getControlDeclaration() { result.asXmlControl() = bindingTarget.getElement() } + + override string toString() { result = bindingTarget.toString() } } class HtmlView extends UI5View, HTML::HtmlFile { @@ -337,6 +495,30 @@ class HtmlView extends UI5View, HTML::HtmlFile { root.isTopLevel() } + HTML::Element getRoot() { result = root } + + override UI5Control getControl() { + exists(HTML::Element element | + result.asXmlControl() = element and + /* Use getAChild+ because some controls nest other controls inside them as aggregations */ + element = root.getChild+() and + ( + /* 1. A builtin control provided by UI5 */ + isBuiltInControl(element.getAttributeByName("sap-ui-type").getValue()) + or + /* 2. A custom control with implementation code found in the webapp */ + /* 2. A custom control with implementation code found in the webapp */ + exists(CustomControl control | + control.getName() = element.getAttributeByName("sap-ui-type").getValue() and + exists(WebApp webApp | + webApp.getAResource() = control.getFile() and + webApp.getAResource() = element.getFile() + ) + ) + ) + ) + } + override string getControllerName() { result = root.getAttributeByName("data-controller-name").getValue() } @@ -347,7 +529,7 @@ class HtmlView extends UI5View, HTML::HtmlFile { type = result.getControlTypeName() and ApiGraphModelsExtensions::sourceModel(getASuperType(type), path, "remote") and property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and - result = control.getAttributeByName("data-" + property) + result.getBindingTarget() = control.getAttributeByName("data-" + property) ) } @@ -357,50 +539,66 @@ class HtmlView extends UI5View, HTML::HtmlFile { type = result.getControlTypeName() and ApiGraphModelsExtensions::sinkModel(getASuperType(type), path, "ui5-html-injection") and property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and - result = control.getAttributeByName("data-" + property) + result.getBindingTarget() = control.getAttributeByName("data-" + property) ) } } -class XmlBindingPath extends UI5BindingPath instanceof XmlAttribute { - string path; +XmlBindingPath getXmlItemsBinding(XmlBindingPath bindingPath) { + exists(Binding itemsBinding | + result != bindingPath and + itemsBinding.getBindingTarget().asXmlAttribute().getName() = "items" and + bindingPath.getBindingTarget().getElement().getParent+().(XmlElement).getAnAttribute() = + itemsBinding.getBindingTarget().asXmlAttribute() and + result = itemsBinding.getBindingPath() + ) and + not exists(bindingPath.getModelName()) +} + +/** + * A UI5BindingPath found in an XML View. + */ +class XmlBindingPath extends UI5BindingPath { + Binding binding; + XmlAttribute bindingTarget; XmlBindingPath() { - path = bindingPathCapture(this.getValue()) and - XmlAttribute.super.getElement().getParent+() instanceof XmlView + bindingTarget = binding.getBindingTarget().asXmlAttribute() and + binding.getBindingPath() = this } - override string toString() { result = XmlAttribute.super.toString() } - - override Location getLocation() { result = XmlAttribute.super.getLocation() } + /* corresponds to BindingPath.asString() */ + override string getLiteralRepr() { result = bindingTarget.getValue() } - override string getPath() { result = path } + override string getPath() { result = this.asString() } + /** + * TODO: take into consideration bindElement() method call + * e.g. + */ override string getAbsolutePath() { - if path.matches("/%") - then result = path + if this.isAbsolute() + then result = this.asString() else - exists(XmlBindingPath composite_path | - composite_path = - XmlAttribute.super.getElement().getParent+().(XmlElement).getAttribute("items") and - result = composite_path.getAbsolutePath() + "/" + path - ) + if exists(getXmlItemsBinding(this)) + then result = getXmlItemsBinding(this).getPath() + "/" + this.getPath() + else result = this.asString() } - override string getPropertyName() { result = XmlAttribute.super.getName() } + override string getPropertyName() { result = bindingTarget.getName() } override string getControlQualifiedType() { exists(XmlElement control | - control = XmlAttribute.super.getElement() and - this = control.getAttribute(this.getPropertyName()) and + control = bindingTarget.getElement() and result = control.getNamespace().getUri() + "." + control.getName() ) } - UI5Control getControl() { - this = result.(XmlElement).getAttribute(this.getPropertyName()) and - result = XmlAttribute.super.getElement() - } + override UI5Control getControlDeclaration() { result.asXmlControl() = bindingTarget.getElement() } + + override string toString() { result = bindingTarget.toString() } + + XmlAttribute getBindingTarget() { result = bindingTarget } } class XmlRootElement extends XmlElement { @@ -408,18 +606,18 @@ class XmlRootElement extends XmlElement { /** * Returns a XML namespace declaration scoped to the element. - * + * * The predicate relies on location information to determine the scope of the namespace declaration. - * A XML element with the same starting line and column, but a larger ending line and column is considered the - * scope of the namespace declaration. + * A XML element with the same starting line and column, but a larger ending line and column is + * considered the scope of the namespace declaration. */ XmlNamespace getANamespaceDeclaration() { exists(Location elemLoc, Location nsLoc | elemLoc = this.getLocation() and nsLoc = result.getLocation() - | - elemLoc.getStartLine() = nsLoc.getStartLine() and - elemLoc.getStartColumn() = nsLoc.getStartColumn() and + | + elemLoc.getStartLine() = nsLoc.getStartLine() and + elemLoc.getStartColumn() = nsLoc.getStartColumn() and ( elemLoc.getEndLine() > nsLoc.getEndLine() or @@ -444,6 +642,8 @@ class XmlView extends UI5View, XmlFile { root.hasName("View") } + XmlElement getRoot() { result = root } + /** Get the qualified type string, e.g. `sap.m.SearchField` */ string getQualifiedType() { result = root.getNamespace().getUri() + "." + root.getName() } @@ -455,7 +655,7 @@ class XmlView extends UI5View, XmlFile { type = result.getControlTypeName() and ApiGraphModelsExtensions::sourceModel(getASuperType(type), path, "remote") and property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and - result = control.getAttribute(property) + result.getBindingTarget() = control.getAttribute(property) ) } @@ -465,160 +665,253 @@ class XmlView extends UI5View, XmlFile { type = result.getControlTypeName() and ApiGraphModelsExtensions::sinkModel(getASuperType(type), path, "ui5-html-injection") and property = path.replaceAll(" ", "").regexpCapture("Member\\[([^\\]]+)\\]", 1) and - result = control.getAttribute(property) - ) - } - - predicate builtInControl(XmlNamespace qualifiedTypeUri) { - exists(string namespace | - namespace = - [ - "sap\\.m.*", // https://sapui5.hana.ondemand.com/#/api/sap.m: The main UI5 control library, with responsive controls that can be used in touch devices as well as desktop browsers. - "sap\\.f.*", // https://sapui5.hana.ondemand.com/#/api/sap.f: SAPUI5 library with controls specialized for SAP Fiori apps. - "sap\\.ui.*" // https://sapui5.hana.ondemand.com/#/api/sap.ui: The sap.ui namespace is the central OpenAjax compliant entry point for UI related JavaScript functionality provided by SAP. - ] - | - qualifiedTypeUri.getUri().regexpMatch(namespace) + result.getBindingTarget() = control.getAttribute(property) and + /* If the control is an `sap.ui.core.HTML` then the control should be missing the `sanitizeContent` attribute */ + ( + getASuperType(type) = "HTMLControl" + implies + ( + not exists(control.getAttribute("sanitizeContent")) or + control.getAttribute("sanitizeContent").getValue() = "false" + ) + ) ) } /** * Get the XML tags associated with UI5 Controls declared in this XML view. */ - XmlControl getXmlControl() { - result = - any(XmlElement element | - // getAChild+ because "container controls" nest other controls inside them - element = root.getAChild+() and - // Either a builtin control provided by UI5 - ( - builtInControl(element.getNamespace()) - or - // or a custom control with implementation code found in the webapp - exists(CustomControl control | - control.getName() = element.getNamespace().getUri() + "." + element.getName() and - exists(WebApp webApp | - webApp.getAResource() = control.getFile() and - webApp.getAResource() = element.getFile() - ) + override UI5Control getControl() { + exists(XmlElement element | + result.asXmlControl() = element and + /* Use getAChild+ because some controls nest other controls inside them as aggregations */ + element = root.getAChild+() and + ( + /* 1. A builtin control provided by UI5 */ + isBuiltInControl(element.getNamespace().getUri()) + or + /* 2. A custom control with implementation code found in the webapp */ + exists(CustomControl control | + control.getName() = element.getNamespace().getUri() + "." + element.getName() and + exists(WebApp webApp | + webApp.getAResource() = control.getFile() and + webApp.getAResource() = element.getFile() ) ) ) + ) } } -abstract class UI5Control extends Locatable { - /** Get the qualified type string, e.g. `sap.m.SearchField` */ - abstract string getQualifiedType(); - - /** Get the qualified type name, e.g. `sap/m/SearchField` */ - string getTypeName() { result = this.getQualifiedType().replaceAll(".", "/") } - - /** Get the JS Control definition if this is a custom control. */ - abstract Extension getJSDefinition(); - - /** Get a reference to this control in the controller code. Currently supports only such references made through `byId`. */ - MethodCallNode getAReference() { - result.getEnclosingFunction() = any(CustomController controller).getAMethod().asExpr() and - result.getMethodName() = "byId" and - result.getArgument(0).asExpr().(StringLiteral).getValue() = this.getAProperty("id").getValue() +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() + ) } - /** Get a property of this control having the name. */ - abstract UI5ControlProperty getAProperty(string propName); +class UI5Control extends TUI5Control { + XmlElement asXmlControl() { this = TXmlControl(result) } - /** Get the definition of this control, given that it's a user-defined one. */ - abstract CustomControl getDefinition(); - - bindingset[propName] - abstract MethodCallNode getARead(string propName); + JsonObject asJsonControl() { this = TJsonControl(result) } - bindingset[propName] - abstract MethodCallNode getAWrite(string propName); - - /** Holds if this control reads from or writes to a model. */ - abstract predicate accessesModel(UI5Model model); + NewNode asJsControl() { this = TJsControl(result) } - /** Holds if this control reads from or writes to a model with regards to a binding path. */ - abstract predicate accessesModel(UI5Model model, UI5BindingPath bindingPath); - - /** Get the view that this control is part of. */ - abstract UI5View getView(); - - /** Get the controller that manages this control. */ - CustomController getController() { result = this.getView().getController() } -} - -class XmlControl extends UI5Control instanceof XmlElement { - XmlControl() { this.getParent+() = any(XmlView view) } + string toString() { + result = this.asXmlControl().toString() + or + result = this.asJsonControl().toString() + or + result = this.asJsControl().toString() + } - /** Get the qualified type string, e.g. `sap.m.SearchField` */ - override string getQualifiedType() { - result = XmlElement.super.getNamespace().getUri() + "." + XmlElement.super.getName() + 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) } - /** Get the JS Control definition if this is a custom control. */ - override Extension getJSDefinition() { - result = any(CustomControl control | control.getName() = this.getQualifiedType()) + /** + * 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() + ) } - override Location getLocation() { result = this.(XmlElement).getLocation() } + File getFile() { + result = this.asXmlControl().getFile() or + result = this.asJsonControl().getFile() or + result = this.asJsControl().getFile() + } - override XmlFile getFile() { result = XmlElement.super.getFile() } + /** + * Gets the `id` property of this control. + */ + string getId() { result = this.getProperty("id").getValue() } - override UI5ControlProperty getAProperty(string name) { result = this.(XmlElement).getAttribute(name) } + /** + * Gets the qualified type name, e.g. `sap/m/SearchField`. + */ + string getImportPath() { result = this.getQualifiedType().replaceAll(".", "/") } - override CustomControl getDefinition() { + /** + * Gets the definition of this control if this is a custom one. + */ + CustomControl getDefinition() { result.getName() = this.getQualifiedType() and exists(WebApp webApp | - webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() + webApp.getAResource() = this.getFile() and + webApp.getAResource() = 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] - override MethodCallNode getARead(string propName) { + MethodCallNode getARead(string propName) { // TODO: in same view exists(WebApp webApp | - webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() + webApp.getAResource() = this.getFile() and + webApp.getAResource() = result.getFile() ) and result.getMethodName() = "get" + capitalize(propName) } bindingset[propName] - override MethodCallNode getAWrite(string propName) { + MethodCallNode getAWrite(string propName) { // TODO: in same view exists(WebApp webApp | - webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile() + webApp.getAResource() = this.getFile() and + webApp.getAResource() = result.getFile() ) and result.getMethodName() = "set" + capitalize(propName) } - override predicate accessesModel(UI5Model model) { + /** Holds if this control reads from or writes to a model. */ + predicate accessesModel(UI5Model model) { 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.getXmlControl() and + this = view.getControl() and model = view.getController().getModel() and - model.getPathString() = XmlElement.super.getAnAttribute().(XmlBindingPath).getPath() + model.(UI5InternalModel).getPathString() = bindingPath.getPath() and + bindingPath.getBindingTarget() = this.asXmlControl().getAnAttribute() ) - // TODO: Add case where modelName is present } - override predicate accessesModel(UI5Model model, UI5BindingPath 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.getXmlControl() and - model = view.getController().getModel() and - model.getPathString() = bindingPath.getPath() and - bindingPath.getPath() = XmlElement.super.getAnAttribute().(XmlBindingPath).getPath() - ) - // TODO: Add case where modelName is present + /** 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() } +} + +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().toString() or + result = this.asJsonControlProperty().toString() or + result = this.asJsControlProperty().toString() } - override UI5View getView() { result = XmlElement.super.getParent+() } + UI5Control getControl() { + result.asXmlControl() = this.asXmlControlProperty().getElement() or + result.asJsonControl() = this.asJsonControlProperty().getParent() or + result.asJsControl().getArgument(0).asExpr() = this.asJsControlProperty().getEnclosingExpr() + } - override string toString() { result = XmlElement.super.toString() } + 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() + } } /** @@ -631,22 +924,22 @@ private string handlerNotationCaptureName(string notation) { } /** - * Function referenced in a Control property. - * e.g. the function `doSomething()` referred in `