Skip to content

Commit 267a929

Browse files
committed
Extract UI5Control to a separate file
1 parent d25c721 commit 267a929

3 files changed

Lines changed: 295 additions & 296 deletions

File tree

javascript/frameworks/ui5/lib/advanced_security/javascript/frameworks/ui5/UI5.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import advanced_security.javascript.frameworks.ui5.JsonParser
44
import advanced_security.javascript.frameworks.ui5.dataflow.TypeTrackers
55
import semmle.javascript.security.dataflow.DomBasedXssCustomizations
66
import advanced_security.javascript.frameworks.ui5.UI5View
7+
import advanced_security.javascript.frameworks.ui5.UI5Control
78
import advanced_security.javascript.frameworks.ui5.UI5HTML
89
import codeql.util.FileSystem
910
private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import advanced_security.javascript.frameworks.ui5.UI5
2+
3+
private newtype TUI5Control =
4+
TXmlControl(XmlElement control) {
5+
control
6+
.(Locatable)
7+
.getFile()
8+
.getBaseName()
9+
.matches(["%.view.xml", "%.view.html", "%.fragment.xml"])
10+
} or
11+
TJsonControl(JsonObject control) {
12+
exists(JsonView view | control.getParent() = view.getRoot().getPropValue("content"))
13+
} or
14+
TJsControl(NewNode control) {
15+
exists(JsView view |
16+
control.asExpr().getParentExpr() =
17+
view.getRoot()
18+
.getArgument(1)
19+
.getALocalSource()
20+
.(ObjectLiteralNode)
21+
.getAPropertyWrite("createContent")
22+
.getRhs()
23+
.(FunctionNode)
24+
.getReturnNode()
25+
.getALocalSource()
26+
.(ArrayLiteralNode)
27+
.asExpr()
28+
)
29+
}
30+
31+
/**
32+
* Models UI5 Controls
33+
* https://sapui5.hana.ondemand.com/sdk/#/topic/8dcab0011d274051808f959800cabf9f
34+
*/
35+
class UI5Control extends TUI5Control {
36+
XmlElement asXmlControl() { this = TXmlControl(result) }
37+
38+
JsonObject asJsonControl() { this = TJsonControl(result) }
39+
40+
NewNode asJsControl() { this = TJsControl(result) }
41+
42+
string toString() {
43+
result = this.asXmlControl().toString()
44+
or
45+
result = this.asJsonControl().toString()
46+
or
47+
result = this.asJsControl().toString()
48+
}
49+
50+
predicate hasLocationInfo(
51+
string filepath, int startcolumn, int startline, int endcolumn, int endline
52+
) {
53+
this.asXmlControl().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
54+
or
55+
/* Since JsonValue does not implement `hasLocationInfo`, we use `getLocation` instead. */
56+
exists(Location location | location = this.asJsonControl().getLocation() |
57+
location.getFile().getAbsolutePath() = filepath and
58+
location.getStartColumn() = startcolumn and
59+
location.getStartLine() = startline and
60+
location.getEndColumn() = endcolumn and
61+
location.getEndLine() = endline
62+
)
63+
or
64+
this.asJsControl().hasLocationInfo(filepath, startcolumn, startline, endcolumn, endline)
65+
}
66+
67+
/**
68+
* Gets the qualified type string, e.g. `sap.m.SearchField`.
69+
*/
70+
string getQualifiedType() {
71+
exists(XmlElement control | control = this.asXmlControl() |
72+
result = control.getNamespace().getUri() + "." + control.getName()
73+
)
74+
or
75+
exists(JsonObject control | control = this.asJsonControl() |
76+
result = control.getPropStringValue("Type")
77+
)
78+
or
79+
exists(NewNode control | control = this.asJsControl() |
80+
result = this.asJsControl().asExpr().getAChildExpr().(DotExpr).getQualifiedName()
81+
)
82+
}
83+
84+
File getFile() {
85+
result = this.asXmlControl().getFile() or
86+
result = this.asJsonControl().getFile() or
87+
result = this.asJsControl().getFile()
88+
}
89+
90+
/**
91+
* Gets the `id` property of this control.
92+
*/
93+
string getId() { result = this.getProperty("id").getValue() }
94+
95+
/**
96+
* Gets the qualified type name, e.g. `sap/m/SearchField`.
97+
*/
98+
string getImportPath() { result = this.getQualifiedType().replaceAll(".", "/") }
99+
100+
/**
101+
* Gets the definition of this control if this is a custom one.
102+
*/
103+
CustomControl getDefinition() {
104+
result.getName() = this.getQualifiedType() and
105+
inSameWebApp(this.getFile(), result.getFile())
106+
}
107+
108+
/**
109+
* Gets a reference to this control. Currently supports only such references made through `byId`.
110+
* Handles both:
111+
* - `this.byId("controlId")` or `this.getView().byId("controlId")` - ID in argument 0
112+
* - `Fragment.byId(viewId, "controlId")` - ID in argument 1
113+
*/
114+
ControlReference getAReference() {
115+
result.getMethodName() = "byId" and
116+
(
117+
// Standard byId: ID in first argument
118+
result.getNumArgument() = 1 and
119+
result.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() =
120+
this.getProperty("id").getValue()
121+
or
122+
// Fragment.byId: ID in second argument
123+
result.getNumArgument() = 2 and
124+
result.getArgument(1).getALocalSource().asExpr().(StringLiteral).getValue() =
125+
this.getProperty("id").getValue()
126+
)
127+
}
128+
129+
/** Gets a property of this control having the name. */
130+
UI5ControlProperty getProperty(string propName) {
131+
result.asXmlControlProperty() = this.asXmlControl().getAttribute(propName)
132+
or
133+
result.asJsonControlProperty() = this.asJsonControl().getPropValue(propName)
134+
or
135+
result.asJsControlProperty() =
136+
this.asJsControl()
137+
.getArgument(0)
138+
.getALocalSource()
139+
.asExpr()
140+
.(ObjectExpr)
141+
.getPropertyByName(propName)
142+
.getAChildExpr()
143+
.flow() and
144+
not exists(Property property | result.asJsControlProperty() = property.getNameExpr().flow())
145+
}
146+
147+
/** Gets a property of this control. */
148+
UI5ControlProperty getAProperty() { result = this.getProperty(_) }
149+
150+
bindingset[propName]
151+
MethodCallNode getARead(string propName) {
152+
// TODO: in same view
153+
inSameWebApp(this.getFile(), result.getFile()) and
154+
result.getMethodName() = "get" + capitalize(propName)
155+
}
156+
157+
bindingset[propName]
158+
MethodCallNode getAWrite(string propName) {
159+
// TODO: in same view
160+
inSameWebApp(this.getFile(), result.getFile()) and
161+
result.getMethodName() = "set" + capitalize(propName)
162+
}
163+
164+
/** Holds if this control reads from or writes to a model. */
165+
predicate accessesModel(UI5Model model) { this.accessesModel(model, _) }
166+
167+
/** Holds if this control reads from or writes to a model with regards to a binding path. */
168+
predicate accessesModel(UI5Model model, XmlBindingPath bindingPath) {
169+
// Verify that the controller's model has the referenced property
170+
exists(XmlView view |
171+
// Both this control and the model belong to the same view
172+
this = view.getControl() and
173+
model = view.getController().getModel() and
174+
model.(UI5InternalModel).getPathString() = bindingPath.getPath() and
175+
bindingPath.getBindingTarget() = this.asXmlControl().getAnAttribute()
176+
)
177+
}
178+
179+
/** Get the view that this control is part of. */
180+
UI5View getView() { result = this.asXmlControl().getFile() }
181+
182+
/** Get the controller that manages this control. */
183+
CustomController getController() { result = this.getView().getController() }
184+
185+
/**
186+
* Gets the full import path of the associated control.
187+
*/
188+
string getControlTypeName() { result = this.getQualifiedType().replaceAll(".", "/") }
189+
190+
/**
191+
* Holds if the control content is sanitized for HTML
192+
* 'sap/ui/core/HTML' sanitized using the property 'sanitizeContent'
193+
* 'sap/ui/richttexteditor/RichTextEditor' sanitized using the property 'sanitizeValue'
194+
*/
195+
predicate isHTMLSanitized() {
196+
this.getControlTypeName() = "sap/ui/richtexteditor/RichTextEditor" and
197+
not this.isSanitizePropertySetTo("sanitizeValue", false)
198+
or
199+
this.getControlTypeName() = "sap/ui/core/HTML" and
200+
this.isSanitizePropertySetTo("sanitizeContent", true) and
201+
not this.isSanitizePropertySetTo("sanitizeContent", false)
202+
}
203+
204+
bindingset[propName, val]
205+
private predicate isSanitizePropertySetTo(string propName, boolean val) {
206+
/* 1. `sanitizeContent` attribute is set declaratively. */
207+
this.getProperty(propName).toString() = val.toString()
208+
or
209+
/* 2. `sanitizeContent` attribute is set programmatically using setProperty(). */
210+
exists(CallNode node | node = this.getAReference().getAMemberCall("setProperty") |
211+
node.getArgument(0).getStringValue() = propName and
212+
not node.getArgument(1).mayHaveBooleanValue(val.booleanNot())
213+
)
214+
or
215+
/* 3. `sanitizeContent` attribute is set programmatically using a setter. */
216+
exists(CallNode node |
217+
node = this.getAReference().getAMemberCall("setS" + propName.suffix(1)) and
218+
not node.getArgument(0).mayHaveBooleanValue(val.booleanNot())
219+
)
220+
}
221+
}
222+
223+
private newtype TUI5ControlProperty =
224+
TXmlControlProperty(XmlAttribute property) or
225+
TJsonControlProperty(JsonValue property) or
226+
TJsControlProperty(ValueNode property)
227+
228+
class UI5ControlProperty extends TUI5ControlProperty {
229+
XmlAttribute asXmlControlProperty() { this = TXmlControlProperty(result) }
230+
231+
JsonValue asJsonControlProperty() { this = TJsonControlProperty(result) }
232+
233+
ValueNode asJsControlProperty() { this = TJsControlProperty(result) }
234+
235+
string toString() {
236+
result = this.asXmlControlProperty().getValue().toString() or
237+
result = this.asJsonControlProperty().toString() or
238+
result = this.asJsControlProperty().toString()
239+
}
240+
241+
UI5Control getControl() {
242+
result.asXmlControl() = this.asXmlControlProperty().getElement() or
243+
result.asJsonControl() = this.asJsonControlProperty().getParent() or
244+
result.asJsControl().getArgument(0).asExpr() = this.asJsControlProperty().getEnclosingExpr()
245+
}
246+
247+
string getName() {
248+
result = this.asXmlControlProperty().getName()
249+
or
250+
exists(JsonValue parent | parent.getPropValue(result) = this.asJsonControlProperty())
251+
or
252+
exists(Property property |
253+
property.getAChildExpr() = this.asJsControlProperty().asExpr() and result = property.getName()
254+
)
255+
}
256+
257+
string getValue() {
258+
result = this.asXmlControlProperty().getValue() or
259+
result = this.asJsonControlProperty().getStringValue() or
260+
result = this.asJsControlProperty().asExpr().(StringLiteral).getValue()
261+
}
262+
}
263+
264+
/**
265+
* Utility predicate capturing the handler name.
266+
*/
267+
bindingset[notation]
268+
private string handlerNotationCaptureName(string notation) {
269+
result =
270+
notation.replaceAll(" ", "").regexpCapture("\\.([\\w-]+)(?:\\([^)]*\\$(\\{[^}]+}).*)?", 1)
271+
}
272+
273+
/**
274+
* A function mentioned in a property of a UI5Control, usually an event handler.
275+
*
276+
* e.g. The function referred to by `doSomething()` as in `<Button press=".doSomething"/>`.
277+
*/
278+
class UI5Handler extends FunctionNode {
279+
UI5Control control;
280+
281+
UI5Handler() {
282+
this = control.getController().getAMethod() and
283+
handlerNotationCaptureName(control.getProperty(_).getValue()) = this.getName()
284+
}
285+
286+
UI5BindingPath getBindingPath() {
287+
exists(string propName |
288+
handlerNotationCaptureName(control.getProperty(propName).getValue()) = this.getName() and
289+
result.getLiteralRepr() = control.getProperty(result.getPropertyName()).getValue()
290+
)
291+
}
292+
293+
UI5Control getControl() { result = control }
294+
}

0 commit comments

Comments
 (0)