Skip to content

Commit 6a00000

Browse files
committed
Exclude sanitized HTML views from XSS sinks
1 parent 0a7b668 commit 6a00000

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+352
-12089
lines changed

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
@@ -3,6 +3,7 @@ import DataFlow
33
import advanced_security.javascript.frameworks.ui5.JsonParser
44
import semmle.javascript.security.dataflow.DomBasedXssCustomizations
55
import advanced_security.javascript.frameworks.ui5.UI5View
6+
import advanced_security.javascript.frameworks.ui5.UI5Control
67
import advanced_security.javascript.frameworks.ui5.UI5HTML
78
import codeql.util.FileSystem
89
private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import advanced_security.javascript.frameworks.ui5.UI5
2+
3+
private newtype TUI5Control =
4+
TXmlControl(XmlElement control) or
5+
TJsonControl(JsonObject control) {
6+
exists(JsonView view | control.getParent() = view.getRoot().getPropValue("content"))
7+
} or
8+
TJsControl(NewNode control) {
9+
exists(JsView view |
10+
control.asExpr().getParentExpr() =
11+
view.getRoot()
12+
.getArgument(1)
13+
.getALocalSource()
14+
.(ObjectLiteralNode)
15+
.getAPropertyWrite("createContent")
16+
.getRhs()
17+
.(FunctionNode)
18+
.getReturnNode()
19+
.getALocalSource()
20+
.(ArrayLiteralNode)
21+
.asExpr()
22+
)
23+
}
24+
25+
class UI5Control extends TUI5Control {
26+
XmlElement asXmlControl() { this = TXmlControl(result) }
27+
28+
JsonObject asJsonControl() { this = TJsonControl(result) }
29+
30+
NewNode asJsControl() { this = TJsControl(result) }
31+
32+
string toString() {
33+
result = this.asXmlControl().toString()
34+
or
35+
result = this.asJsonControl().toString()
36+
or
37+
result = this.asJsControl().toString()
38+
}
39+
40+
predicate hasLocationInfo(
41+
string filepath, int startcolumn, int startline, int endcolumn, int endline
42+
) {
43+
this.asXmlControl().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
44+
or
45+
/* Since JsonValue does not implement `hasLocationInfo`, we use `getLocation` instead. */
46+
exists(Location location | location = this.asJsonControl().getLocation() |
47+
location.getFile().getAbsolutePath() = filepath and
48+
location.getStartColumn() = startcolumn and
49+
location.getStartLine() = startline and
50+
location.getEndColumn() = endcolumn and
51+
location.getEndLine() = endline
52+
)
53+
or
54+
this.asJsControl().hasLocationInfo(filepath, startcolumn, startline, endcolumn, endline)
55+
}
56+
57+
/**
58+
* Gets the qualified type string, e.g. `sap.m.SearchField`.
59+
*/
60+
string getQualifiedType() {
61+
exists(XmlElement control | control = this.asXmlControl() |
62+
result = control.getNamespace().getUri() + "." + control.getName()
63+
)
64+
or
65+
exists(JsonObject control | control = this.asJsonControl() |
66+
result = control.getPropStringValue("Type")
67+
)
68+
or
69+
exists(NewNode control | control = this.asJsControl() |
70+
result = this.asJsControl().asExpr().getAChildExpr().(DotExpr).getQualifiedName()
71+
)
72+
}
73+
74+
File getFile() {
75+
result = this.asXmlControl().getFile() or
76+
result = this.asJsonControl().getFile() or
77+
result = this.asJsControl().getFile()
78+
}
79+
80+
/**
81+
* Gets the `id` property of this control.
82+
*/
83+
string getId() { result = this.getProperty("id").getValue() }
84+
85+
/**
86+
* Gets the qualified type name, e.g. `sap/m/SearchField`.
87+
*/
88+
string getImportPath() { result = this.getQualifiedType().replaceAll(".", "/") }
89+
90+
/**
91+
* Gets the definition of this control if this is a custom one.
92+
*/
93+
CustomControl getDefinition() {
94+
result.getName() = this.getQualifiedType() and
95+
inSameWebApp(this.getFile(), result.getFile())
96+
}
97+
98+
/**
99+
* Gets a reference to this control. Currently supports only such references made through `byId`.
100+
*/
101+
ControlReference getAReference() {
102+
result.getMethodName() = "byId" and
103+
result.getArgument(0).getALocalSource().asExpr().(StringLiteral).getValue() =
104+
this.getProperty("id").getValue()
105+
}
106+
107+
/** Gets a property of this control having the name. */
108+
UI5ControlProperty getProperty(string propName) {
109+
result.asXmlControlProperty() = this.asXmlControl().getAttribute(propName)
110+
or
111+
result.asJsonControlProperty() = this.asJsonControl().getPropValue(propName)
112+
or
113+
result.asJsControlProperty() =
114+
this.asJsControl()
115+
.getArgument(0)
116+
.getALocalSource()
117+
.asExpr()
118+
.(ObjectExpr)
119+
.getPropertyByName(propName)
120+
.getAChildExpr()
121+
.flow() and
122+
not exists(Property property | result.asJsControlProperty() = property.getNameExpr().flow())
123+
}
124+
125+
/** Gets a property of this control. */
126+
UI5ControlProperty getAProperty() { result = this.getProperty(_) }
127+
128+
bindingset[propName]
129+
MethodCallNode getARead(string propName) {
130+
// TODO: in same view
131+
inSameWebApp(this.getFile(), result.getFile()) and
132+
result.getMethodName() = "get" + capitalize(propName)
133+
}
134+
135+
bindingset[propName]
136+
MethodCallNode getAWrite(string propName) {
137+
// TODO: in same view
138+
inSameWebApp(this.getFile(), result.getFile()) and
139+
result.getMethodName() = "set" + capitalize(propName)
140+
}
141+
142+
/** Holds if this control reads from or writes to a model. */
143+
predicate accessesModel(UI5Model model) { accessesModel(model, _) }
144+
145+
/** Holds if this control reads from or writes to a model with regards to a binding path. */
146+
predicate accessesModel(UI5Model model, XmlBindingPath bindingPath) {
147+
// Verify that the controller's model has the referenced property
148+
exists(XmlView view |
149+
// Both this control and the model belong to the same view
150+
this = view.getControl() and
151+
model = view.getController().getModel() and
152+
model.(UI5InternalModel).getPathString() = bindingPath.getPath() and
153+
bindingPath.getBindingTarget() = this.asXmlControl().getAnAttribute()
154+
)
155+
}
156+
157+
/** Get the view that this control is part of. */
158+
UI5View getView() { result = this.asXmlControl().getFile() }
159+
160+
/** Get the controller that manages this control. */
161+
CustomController getController() { result = this.getView().getController() }
162+
163+
/**
164+
* Gets the full import path of the associated control.
165+
*/
166+
string getControlTypeName() { result = this.getQualifiedType().replaceAll(".", "/") }
167+
168+
/**
169+
* Holds if the attribute `sanitizeContent`
170+
* in controls `sap.ui.core.HTML` and `sap.ui.richttexteditor.RichTextEditor`
171+
* is set to true and never set to false anywhere
172+
*/
173+
predicate isSanitizedControl() {
174+
not this = sanitizeContentSetTo(false) and
175+
(
176+
this.getControlTypeName() = "sap/ui/richttexteditor/RichTextEditor"
177+
or
178+
this.getControlTypeName() = "sap/ui/core/HTML" and
179+
this = sanitizeContentSetTo(true)
180+
)
181+
}
182+
183+
bindingset[val]
184+
private UI5Control sanitizeContentSetTo(boolean val) {
185+
// `sanitizeContent` attribute is set declaratively
186+
result.getProperty("sanitizeContent").toString() = val.toString()
187+
or
188+
//or
189+
// `sanitizeContent` attribute is set programmatically (not sufficient)
190+
//result
191+
// .getAReference()
192+
// .hasPropertyWrite("sanitizeContent",
193+
// any(DataFlow::Node n | not n.mayHaveBooleanValue(val.booleanNot())))
194+
// `sanitizeContent` attribute is set programmatically using setProperty()
195+
exists(CallNode node | node = result.getAReference().getAMemberCall("setProperty") |
196+
node.getArgument(0).getStringValue() = "sanitizeContent" and
197+
not node.getArgument(1).mayHaveBooleanValue(val.booleanNot())
198+
)
199+
}
200+
}
201+
202+
class SanitizedUI5Control extends UI5Control {
203+
SanitizedUI5Control() { super.isSanitizedControl() }
204+
}
205+
206+
private newtype TUI5ControlProperty =
207+
TXmlControlProperty(XmlAttribute property) or
208+
TJsonControlProperty(JsonValue property) or
209+
TJsControlProperty(ValueNode property)
210+
211+
class UI5ControlProperty extends TUI5ControlProperty {
212+
XmlAttribute asXmlControlProperty() { this = TXmlControlProperty(result) }
213+
214+
JsonValue asJsonControlProperty() { this = TJsonControlProperty(result) }
215+
216+
ValueNode asJsControlProperty() { this = TJsControlProperty(result) }
217+
218+
string toString() {
219+
result = this.asXmlControlProperty().getValue().toString() or
220+
result = this.asJsonControlProperty().toString() or
221+
result = this.asJsControlProperty().toString()
222+
}
223+
224+
UI5Control getControl() {
225+
result.asXmlControl() = this.asXmlControlProperty().getElement() or
226+
result.asJsonControl() = this.asJsonControlProperty().getParent() or
227+
result.asJsControl().getArgument(0).asExpr() = this.asJsControlProperty().getEnclosingExpr()
228+
}
229+
230+
string getName() {
231+
result = this.asXmlControlProperty().getName()
232+
or
233+
exists(JsonValue parent | parent.getPropValue(result) = this.asJsonControlProperty())
234+
or
235+
exists(Property property |
236+
property.getAChildExpr() = this.asJsControlProperty().asExpr() and result = property.getName()
237+
)
238+
}
239+
240+
string getValue() {
241+
result = this.asXmlControlProperty().getValue() or
242+
result = this.asJsonControlProperty().getStringValue() or
243+
result = this.asJsControlProperty().asExpr().(StringLiteral).getValue()
244+
}
245+
}

0 commit comments

Comments
 (0)