Skip to content

Commit 3025468

Browse files
committed
JS: Move ".."-parsing trick into AccessPathSyntax.qll
1 parent 7c2cff3 commit 3025468

4 files changed

Lines changed: 69 additions & 26 deletions

File tree

javascript/ql/lib/semmle/javascript/frameworks/data/internal/AccessPathSyntax.qll

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,31 @@ module AccessPath {
2222
class AccessPath extends string instanceof AccessPath::Range {
2323
/** Gets the `n`th token on the access path as a string. */
2424
string getRawToken(int n) {
25-
this != "" and // The empty path should have zero tokens, not a single empty token
26-
result = this.splitAt(".", n)
25+
// Avoid splitting by '.' since tokens may contain dots, e.g. `Field[foo.Bar.x]`.
26+
// Instead use regexpFind to match valid tokens, and supplement with a final length
27+
// check to ensure all characters were included in a token.
28+
result = this.regexpFind("\\w+(?:\\[[^\\]]*\\])?(?=\\.|$)", n, _)
2729
}
2830

29-
/** Gets the `n`th token on the access path. */
30-
AccessPathToken getToken(int n) { result = this.getRawToken(n) }
31+
/** Holds if this string is not a syntactically valid access path. */
32+
predicate hasSyntaxError() {
33+
// If the lengths match, all characters must haven been included in a token
34+
// or seen by the `.` lookahead pattern.
35+
this != "" and
36+
not this.length() = sum(int n | | getRawToken(n).length() + 1) - 1
37+
}
38+
39+
/** Gets the `n`th token on the access path (if there are no syntax errors). */
40+
AccessPathToken getToken(int n) {
41+
result = this.getRawToken(n) and
42+
not hasSyntaxError()
43+
}
3144

32-
/** Gets the number of tokens on the path. */
33-
int getNumToken() { result = count(int n | exists(this.getRawToken(n))) }
45+
/** Gets the number of tokens on the path (if there are no syntax errors). */
46+
int getNumToken() {
47+
result = count(int n | exists(this.getRawToken(n))) and
48+
not hasSyntaxError()
49+
}
3450
}
3551

3652
/**

javascript/ql/lib/semmle/javascript/frameworks/data/internal/Shared.qll

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -163,19 +163,13 @@ private predicate summaryModel(string row) { any(SummaryModelCsv s).row(inverseP
163163

164164
private predicate typeModel(string row) { any(TypeModelCsv s).row(inversePad(row)) }
165165

166-
/**
167-
* Replaces `..` with `-->` in order to simplify subsequent parsing.
168-
*/
169-
bindingset[path]
170-
private string normalizePath(string path) { result = path.replaceAll("..", "-->") }
171-
172166
/** Holds if a source model exists for the given parameters. */
173167
predicate sourceModel(string package, string type, string path, string kind) {
174168
exists(string row |
175169
sourceModel(row) and
176170
row.splitAt(";", 0) = package and
177171
row.splitAt(";", 1) = type and
178-
normalizePath(row.splitAt(";", 2)) = path and
172+
row.splitAt(";", 2) = path and
179173
row.splitAt(";", 3) = kind
180174
)
181175
}
@@ -186,7 +180,7 @@ private predicate sinkModel(string package, string type, string path, string kin
186180
sinkModel(row) and
187181
row.splitAt(";", 0) = package and
188182
row.splitAt(";", 1) = type and
189-
normalizePath(row.splitAt(";", 2)) = path and
183+
row.splitAt(";", 2) = path and
190184
row.splitAt(";", 3) = kind
191185
)
192186
}
@@ -199,9 +193,9 @@ private predicate summaryModel(
199193
summaryModel(row) and
200194
row.splitAt(";", 0) = package and
201195
row.splitAt(";", 1) = type and
202-
normalizePath(row.splitAt(";", 2)) = path and
203-
normalizePath(row.splitAt(";", 3)) = input and
204-
normalizePath(row.splitAt(";", 4)) = output and
196+
row.splitAt(";", 2) = path and
197+
row.splitAt(";", 3) = input and
198+
row.splitAt(";", 4) = output and
205199
row.splitAt(";", 5) = kind
206200
)
207201
}
@@ -216,7 +210,7 @@ private predicate typeModel(
216210
row.splitAt(";", 1) = type1 and
217211
row.splitAt(";", 2) = package2 and
218212
row.splitAt(";", 3) = type2 and
219-
normalizePath(row.splitAt(";", 4)) = path
213+
row.splitAt(";", 4) = path
220214
)
221215
}
222216

@@ -434,9 +428,9 @@ bindingset[arg]
434428
private int getAnIntFromString(string arg) {
435429
result = arg.toInt()
436430
or
437-
// Match "n1..n2", where ".." has previously been replaced with "-->" to simplify parsing
431+
// Match "n1..n2"
438432
exists(string lo, string hi |
439-
regexpCaptureTwo(arg, "(\\d+)-->(\\d+)", lo, hi) and
433+
regexpCaptureTwo(arg, "(\\d+)\\.\\.(\\d+)", lo, hi) and
440434
result = [lo.toInt() .. hi.toInt()]
441435
)
442436
}
@@ -446,8 +440,8 @@ private int getAnIntFromString(string arg) {
446440
*/
447441
bindingset[arg]
448442
private int getLowerBoundFromString(string arg) {
449-
// Match "n..", where ".." has previously been replaced with "-->" to simplify parsing
450-
result = arg.regexpCapture("(\\d+)-->", 1).toInt()
443+
// Match "n.."
444+
result = arg.regexpCapture("(\\d+)\\.\\.", 1).toInt()
451445
}
452446

453447
/**
@@ -479,22 +473,22 @@ private int getAnIntFromStringWithArity(string arg, int arity) {
479473
result = arity - lo.toInt()
480474
or
481475
// N-x..
482-
lo = arg.regexpCapture("N-(\\d+)-->", 1) and
476+
lo = arg.regexpCapture("N-(\\d+)\\.\\.", 1) and
483477
result = [arity - lo.toInt(), arity - 1]
484478
)
485479
or
486480
exists(string lo, string hi |
487481
// x..N-y
488-
regexpCaptureTwo(arg, "(\\d+)-->N-(\\d+)", lo, hi) and
482+
regexpCaptureTwo(arg, "(\\d+)\\.\\.N-(\\d+)", lo, hi) and
489483
result = [lo.toInt() .. arity - hi.toInt()]
490484
or
491485
// N-x..Ny
492-
regexpCaptureTwo(arg, "N-(\\d+)-->N-(\\d+)", lo, hi) and
486+
regexpCaptureTwo(arg, "N-(\\d+)\\.\\.N-(\\d+)", lo, hi) and
493487
result = [arity - lo.toInt() .. arity - hi.toInt()] and
494488
result >= 0
495489
or
496490
// N-x..y
497-
regexpCaptureTwo(arg, "N-(\\d+)-->(\\d+)", lo, hi) and
491+
regexpCaptureTwo(arg, "N-(\\d+)\\.\\.(\\d+)", lo, hi) and
498492
result = [arity - lo.toInt() .. hi.toInt()] and
499493
result >= 0
500494
)

javascript/ql/test/library-tests/frameworks/data/test.expected

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,14 @@ isSink
7171
| test.js:78:34:78:34 | 3 | test-sink |
7272
| test.js:81:28:81:35 | source() | test-sink |
7373
| test.js:82:28:82:28 | 1 | test-sink |
74+
syntaxErrors
75+
| Member[foo |
76+
| Member[foo] .Member[bar] |
77+
| Member[foo] Member[bar] |
78+
| Member[foo], Member[bar] |
79+
| Member[foo],Member[bar] |
80+
| Member[foo]. Member[bar] |
81+
| Member[foo]..Member[bar] |
82+
| Member[foo]Member[bar] |
83+
| Member[foo]] |
84+
| Member[foo]].Member[bar] |

javascript/ql/test/library-tests/frameworks/data/test.ql

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import javascript
22
import testUtilities.ConsistencyChecking
3+
import semmle.javascript.frameworks.data.internal.AccessPathSyntax as AccessPathSyntax
34

45
class Steps extends ModelInput::SummaryModelCsv {
56
override predicate row(string row) {
@@ -54,3 +55,24 @@ query predicate taintFlow(DataFlow::Node source, DataFlow::Node sink) {
5455
query predicate isSink(DataFlow::Node node, string kind) {
5556
node = ModelOutput::getASinkNode(kind).getARhs()
5657
}
58+
59+
class SyntaxErrorTest extends ModelInput::SinkModelCsv {
60+
override predicate row(string row) {
61+
row = [
62+
"testlib;;Member[foo],Member[bar];test-sink",
63+
"testlib;;Member[foo] Member[bar];test-sink",
64+
"testlib;;Member[foo]. Member[bar];test-sink",
65+
"testlib;;Member[foo], Member[bar];test-sink",
66+
"testlib;;Member[foo]..Member[bar];test-sink",
67+
"testlib;;Member[foo] .Member[bar];test-sink",
68+
"testlib;;Member[foo]Member[bar];test-sink",
69+
"testlib;;Member[foo;test-sink",
70+
"testlib;;Member[foo]];test-sink",
71+
"testlib;;Member[foo]].Member[bar];test-sink"
72+
]
73+
}
74+
}
75+
76+
query predicate syntaxErrors(AccessPathSyntax::AccessPath path) {
77+
path.hasSyntaxError()
78+
}

0 commit comments

Comments
 (0)