Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5ad6961
Add JSON parser
rvermeulen Oct 18, 2023
13839a4
Add source provenance to Json parser
rvermeulen Oct 18, 2023
d57bb93
Add resource root parsing
rvermeulen Oct 18, 2023
a68107f
Remove delimiting quotes from string value
rvermeulen Oct 18, 2023
66bcc79
QL formatting
rvermeulen Oct 18, 2023
10b5ed4
Add support for resolved resource roots
rvermeulen Oct 18, 2023
a5fe10f
Address typo in branch type
rvermeulen Oct 18, 2023
c5b5848
Allow escaped double quotes in strings
rvermeulen Oct 20, 2023
633aa0f
Add getType predicate to get the type of a Json value
rvermeulen Oct 20, 2023
2603198
Make sure the first token check uses the same source
rvermeulen Oct 20, 2023
84f8a4a
Add failing test case
rvermeulen Oct 20, 2023
1b323f3
Add support for strings with escaped double quotes
rvermeulen Oct 23, 2023
2330d1a
Address Json value source mixup
rvermeulen Oct 23, 2023
c7d16e6
Enclose Json string value in double quotes
rvermeulen Oct 23, 2023
962cc18
Remove superfluous predicate evaluation
rvermeulen Oct 23, 2023
1e2c240
Add QLdoc to module
rvermeulen Oct 23, 2023
2405ad7
Encapsulate first token logic in JsonToken
rvermeulen Oct 23, 2023
36bbca7
Formatting
rvermeulen Oct 23, 2023
94bd9fa
Address incorrect string
rvermeulen Oct 23, 2023
b2a04db
Add Json test expected file
rvermeulen Oct 23, 2023
000e62c
Broaden the requirements for an XML view
rvermeulen Oct 16, 2023
9920069
Upgrade CodeQL dependencies and bump pack versions
rvermeulen Oct 24, 2023
7d468a8
Upgrade CLI to 2.15.1
rvermeulen Oct 25, 2023
8ae110d
Add member predicate to interpret Json value as a string value
rvermeulen Oct 23, 2023
4186ebe
Resolve resource roots using Json string value
rvermeulen Oct 23, 2023
feda8e2
Add member predicate to determine if file is part of resource root
rvermeulen Oct 23, 2023
a9a885e
Consider nojQuery Sap Ui core script
rvermeulen Oct 23, 2023
2dbaf58
Replace Project with WebApp
rvermeulen Oct 24, 2023
9311c30
Format QL modules
rvermeulen Oct 24, 2023
3edb117
Address incorrect association between frame options and web app
rvermeulen Oct 27, 2023
9b6b7a3
Merge branch 'main' into rvermeulen/ui5-bootstrap-detection
rvermeulen Oct 27, 2023
09e656d
Remove redundant class JsonStringReader
rvermeulen Nov 20, 2023
5243a48
Simplify ResolvedResourceRoot class
rvermeulen Nov 22, 2023
6289aca
Weaken SAP core script pattern
rvermeulen Nov 22, 2023
796c099
Reneame SapUiCoreScript to SapUiCoreScriptElement
rvermeulen Nov 28, 2023
ceeffa0
Add QLdoc to getInitialModule
rvermeulen Nov 28, 2023
5f94964
Clarify QLdoc of isMissingFrameOptionsToPreventClickjacking
rvermeulen Nov 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,38 +1,139 @@
private import javascript
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 {
/**
* Helper predicate checking if two elements are in the same Project
*/
predicate inSameUI5Project(File f1, File f2) {
exists(Project p | p.isInThisProject(f1) and p.isInThisProject(f2))
private class ResourceRootPathString extends PathString {
SapUiCoreScriptElement coreScript;

ResourceRootPathString() { this = coreScript.getAResourceRoot().getRoot() }

override Folder getARootFolder() { result = coreScript.getFile().getParentContainer() }
}

class Project extends Folder {
/**
* An UI5 project root folder.
*/
Project() { exists(File yamlFile | yamlFile = this.getFile("ui5.yaml")) }
private newtype TResourceRoot =
MkResourceRoot(string name, string root, string source) {
exists(
JsonParser<getAResourceRootConfig/0>::JsonObject config,
JsonParser<getAResourceRootConfig/0>::JsonMember configEntry, SapUiCoreScriptElement coreScript
|
source = coreScript.getAttributeByName("data-sap-ui-resourceroots").getValue() and
source = config.getSource() and
config.getAMember() = configEntry
|
name = configEntry.getKey() and
root = configEntry.getValue().asString()
)
}

class ResourceRoot extends TResourceRoot, MkResourceRoot {
string getName() { this = MkResourceRoot(result, _, _) }

string getRoot() { this = MkResourceRoot(_, result, _) }

string getSource() { this = MkResourceRoot(_, _, result) }

string toString() { result = this.getName() + ": " + this.getRoot() }
}

class ResolvedResourceRoot extends Container {
ResourceRoot unresolvedRoot;
ResolvedResourceRoot() {
exists(ResourceRootPathString resourceRootPathString | unresolvedRoot.getRoot() = resourceRootPathString |
this = resourceRootPathString.resolve(resourceRootPathString.getARootFolder()).getContainer())
}

string getName() {
result = unresolvedRoot.getName()
}

string getSource() {
result = unresolvedRoot.getSource()
}

predicate contains(File file) {
file.getParentContainer+() = this
}
}

private string getAResourceRootConfig() {
result = any(SapUiCoreScriptElement script).getAttributeByName("data-sap-ui-resourceroots").getValue()
}

class SapUiCoreScriptElement extends HTML::ScriptElement {
SapUiCoreScriptElement() {
this.getSourcePath().matches(["%sap-ui-core.js", "%sap-ui-core-nojQuery.js"])
}

ResourceRoot getAResourceRoot() {
result.getSource() = this.getAttributeByName("data-sap-ui-resourceroots").getValue()
}

ResolvedResourceRoot getAResolvedResourceRoot() {
result.getSource() = this.getAttributeByName("data-sap-ui-resourceroots").getValue()
}
}

/** 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()
}

WebApp getWebapp() { result = webapp }
}

/** A UI5 bootstrapped web application. */
class WebApp extends HTML::HtmlFile {
SapUiCoreScriptElement coreScript;

WebApp() { coreScript.getFile() = this }

File getAResource() { coreScript.getAResolvedResourceRoot().contains(result) }

File getResource(string path) {
getWebAppFolder().getAbsolutePath() + "/" + path = result.getAbsolutePath()
}

Folder getWebAppFolder() { result = this.getParentContainer() }

WebAppManifest getManifest() { result.getWebapp() = this }

/**
* The `ui5.yaml` file that declares a UI5 application.
* Gets the JavaScript module that serves as an entrypoint to this webapp.
*/
File getProjectYaml() { result = this.getFile("ui5.yaml") }

predicate isInThisProject(File file) { this = file.getParentContainer*() }
File getInitialModule() {
exists(
string initialModuleResourcePath, string resolvedModulePath,
ResolvedResourceRoot resourceRoot
|
initialModuleResourcePath = coreScript.getAttributeByName("data-sap-ui-onInit").getValue() and
coreScript.getAResolvedResourceRoot() = resourceRoot and
resolvedModulePath =
initialModuleResourcePath
.regexpReplaceAll("^module\\s*:\\s*", "")
.replaceAll(resourceRoot.getName(), resourceRoot.getAbsolutePath()) and
result.getAbsolutePath() = resolvedModulePath + ".js"
)
}

private HTML::HtmlFile getSapUICoreScript() {
exists(HTML::ScriptElement script |
result = script.getFile() and
this.isInThisProject(result) and
script.getSourcePath().matches("%/sap-ui-core.js")
FrameOptions getFrameOptions() {
exists(HTML::DocumentElement doc | doc.getFile() = this |
result.asHtmlFrameOptions() = coreScript.getAnAttribute()
)
or
result.asJsFrameOptions().getFile() = this
}

HTML::HtmlFile getMainHTML() { result = this.getSapUICoreScript() }
HTML::DocumentElement getDocument() {
result.getFile() = this
}
}

/**
Expand Down Expand Up @@ -76,7 +177,7 @@ module UI5 {
)
}

Project getProject() { result = this.getFile().getParentContainer*() }
WebApp getWebApp() { this.getFile() = result.getAResource() }

SapDefineModule getExtendingDefine() {
exists(Extension baseExtension, Extension subclassExtension, SapDefineModule subclassDefine |
Expand Down Expand Up @@ -291,13 +392,7 @@ module UI5 {
*/
bindingset[path]
JsonObject resolveDirectPath(string path) {
exists(Project project, File jsonFile |
// project contains this file
project.isInThisProject(jsonFile) and
jsonFile.getExtension() = "json" and
jsonFile.getAbsolutePath() = project.getASubFolder().getAbsolutePath() + "/" + path and
result.getJsonFile() = jsonFile
)
exists(WebApp webApp | result.getJsonFile() = webApp.getResource(path))
}

/**
Expand Down Expand Up @@ -502,14 +597,18 @@ module UI5 {
result.getMethodName() = "setProperty" and
result.getArgument(0).asExpr().(StringLiteral).getValue() = propName and
// TODO: in same controller
inSameUI5Project(this.getFile(), result.getFile())
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
inSameUI5Project(this.getFile(), result.getFile())
exists(WebApp webApp |
webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ module UI5DataFlow {
private predicate bidiModelControl(DataFlow::Node start, DataFlow::Node end) {
exists(DataFlow::SourceNode property, Metadata metadata, UI5BoundNode node |
// same project
inSameUI5Project(metadata.getFile(), node.getFile()) and
exists(WebApp webApp |
webApp.getAResource() = metadata.getFile() and webApp.getAResource() = node.getFile()
) and
(
// same control
metadata.getControl().getName() = node.getBindingPath().getControlQualifiedType()
Expand Down Expand Up @@ -87,21 +89,24 @@ module UI5DataFlow {
UI5BindingPath getBindingPath() { result = bindingPath }

UI5BoundNode() {
/* 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) and
inSameUI5Project(this.getFile(), bindingPath.getFile())
)
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() and
inSameUI5Project(this.getFile(), bindingPath.getFile())
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()
)
)
}
}
Expand All @@ -112,9 +117,7 @@ module UI5DataFlow {
class UI5ModelSource extends UI5DataFlow::UI5BoundNode, RemoteFlowSource {
UI5ModelSource() { bindingPath = any(UI5View view).getASource() }

override string getSourceType() {
result = "UI5 model remote flow source"
}
override string getSourceType() { result = "UI5 model remote flow source" }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ class FrameOptions extends TFrameOptions {
}

/**
* Holds if the frame options are left untouched as the default value `trusted`.
* Holds if there are no frame options specified to prevent click jacking.
*/
predicate thereIsNoFrameOptionSet(UI5::Project p) {
not exists(FrameOptions frameOptions | p.isInThisProject(frameOptions.getLocation().getFile()) |
predicate isMissingFrameOptionsToPreventClickjacking(UI5::WebApp webapp) {
not exists(FrameOptions frameOptions | webapp.getFrameOptions() = frameOptions |
frameOptions.allowsSharedOriginEmbedding() or
frameOptions.deniesEmbedding() or
frameOptions.allowsAllOriginEmbedding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,10 @@ abstract class UI5BindingPath extends Locatable {
// The property bound to an UI5View source
result.getPropertyNameExpr() = p.getNameExpr() and
this.getAbsolutePath() = model.getPathString(p) and
//restrict search inside the same project
inSameUI5Project(this.getFile(), result.getFile())
//restrict search inside the same webapp
exists(WebApp webApp |
webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile()
)
)
// TODO
/*
Expand Down Expand Up @@ -142,8 +144,10 @@ abstract class UI5View extends File {
CustomController getController() {
// The controller name should match
result.getName() = this.getControllerName() and
// The View and the Controller are in a same project
inSameUI5Project(this, result.getFile())
// The View and the Controller are in a same webapp
exists(WebApp webApp |
webApp.getAResource() = this and webApp.getAResource() = result.getFile()
)
}

abstract UI5BindingPath getASource();
Expand Down Expand Up @@ -490,10 +494,13 @@ class XmlView extends UI5View, XmlFile {
(
builtInControl(element.getNamespace())
or
// or a custom control with implementation code found in the project
// or a custom control with implementation code found in the webapp
exists(CustomControl control |
control.getName() = element.getNamespace().getUri() + "." + element.getName() and
inSameUI5Project(control.getFile(), element.getFile())
exists(WebApp webApp |
webApp.getAResource() = control.getFile() and
webApp.getAResource() = element.getFile()
)
)
)
)
Expand Down Expand Up @@ -563,20 +570,26 @@ class XmlControl extends UI5Control instanceof XmlElement {

override CustomControl getDefinition() {
result.getName() = this.getQualifiedType() and
inSameUI5Project(this.getFile(), result.getFile())
exists(WebApp webApp |
webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile()
)
}

bindingset[propName]
override MethodCallNode getARead(string propName) {
// TODO: in same view
inSameUI5Project(this.getFile(), result.getFile()) and
exists(WebApp webApp |
webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile()
) and
result.getMethodName() = "get" + capitalize(propName)
}

bindingset[propName]
override MethodCallNode getAWrite(string propName) {
// TODO: in same view
inSameUI5Project(this.getFile(), result.getFile()) and
exists(WebApp webApp |
webApp.getAResource() = this.getFile() and webApp.getAResource() = result.getFile()
) and
result.getMethodName() = "set" + capitalize(propName)
}

Expand Down
Loading