Skip to content

Commit 12a579e

Browse files
Add relative filepath lookup
1 parent 40a7223 commit 12a579e

File tree

1 file changed

+76
-3
lines changed

1 file changed

+76
-3
lines changed

csharp/ql/lib/semmle/code/csharp/security/dataflow/XSSFlowSteps.qll

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ private import codeql.util.Unit
33
private import semmle.code.csharp.frameworks.microsoft.AspNetCore
44

55
/** An additional flow step for cross-site scripting (XSS) vulnerabilities */
6-
abstract class XssAdditionalFlowStep extends Unit {
6+
class XssAdditionalFlowStep extends Unit {
77
abstract predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2);
88
}
99

@@ -73,9 +73,82 @@ private class ViewCallFlowStep extends XssAdditionalFlowStep {
7373
}
7474

7575
private predicate viewCallRefersToPage(ViewCall vc, RazorPage rp) {
76-
viewCallRefersToPageAbsolute(vc, rp)
76+
viewCallRefersToPageAbsolute(vc, rp) or
77+
viewCallRefersToPageRelative(vc, rp)
7778
}
7879

7980
private predicate viewCallRefersToPageAbsolute(ViewCall vc, RazorPage rp) {
80-
["/", "~/", ""] + vc.getNameArgument() = rp.getSourceFilepath()
81+
["/", ""] + vc.getNameArgument() = ["", "~"] + rp.getSourceFilepath()
82+
}
83+
84+
private predicate viewCallRefersToPageRelative(ViewCall vc, RazorPage rp) {
85+
rp.getSourceFilepath() =
86+
min(int i, RelativeViewCallFilepath fp |
87+
fp.hasViewCallWithIndex(vc, i) and
88+
exists(RazorPage rp2 | rp2.getSourceFilepath() = fp.getNormalizedPath())
89+
|
90+
fp.getNormalizedPath() order by i
91+
)
92+
}
93+
94+
private class RelativeViewCallFilepath extends NormalizableFilepath {
95+
ViewCall vc;
96+
int idx;
97+
98+
RelativeViewCallFilepath() {
99+
exists(string actionName |
100+
actionName = vc.getNameArgument() and
101+
not actionName.matches("%.cshtml")
102+
or
103+
not exists(vc.getNameArgument()) and
104+
actionName = vc.getActionMethod().getName()
105+
|
106+
idx = 0 and
107+
this = "/Views/" + vc.getControllerName() + "/" + actionName + ".cshtml"
108+
or
109+
idx = 1 and
110+
this = "/Views/Shared/" + actionName + ".cshtml"
111+
)
112+
}
113+
114+
predicate hasViewCallWithIndex(ViewCall vc2, int idx2) { vc = vc2 and idx = idx2 }
115+
}
116+
117+
// TODO: this could be a shared library
118+
/** A filepath that should be normalized. */
119+
abstract private class NormalizableFilepath extends string {
120+
bindingset[this]
121+
NormalizableFilepath() { any() }
122+
123+
/** Gets the normalized filepath for this string; traversing `/../` paths. */
124+
string getNormalizedPath() {
125+
exists(string norm |
126+
norm = this.getNormalizedUpTo(0).regexpReplaceAll("/+$", "") and
127+
(if this.matches("/%") then result = "/" + norm else result = norm)
128+
)
129+
}
130+
131+
private string getComponent(int i) { result = this.splitAt("/", i) }
132+
133+
private int getNumComponents() { result = strictcount(int i | exists(this.getComponent(i))) }
134+
135+
private string getNormalizedUpTo(int i) {
136+
i in [0 .. this.getNumComponents()] and
137+
(
138+
i = this.getNumComponents() and
139+
result = ""
140+
or
141+
i < this.getNumComponents() and
142+
exists(string comp, string sofar |
143+
comp = this.getComponent(i) and sofar = this.getNormalizedUpTo(i + 1)
144+
|
145+
if comp = [".", ""]
146+
then result = sofar
147+
else
148+
if comp = ".." or not sofar.matches("../%")
149+
then result = comp + "/" + sofar
150+
else exists(string base | sofar = "../" + base | result = base)
151+
)
152+
)
153+
}
81154
}

0 commit comments

Comments
 (0)