|
75 | 75 | # "a,limit,b" would false-positive. |
76 | 76 | _CLAUSE_KEYWORDS = frozenset(("FROM", "WHERE", "HAVING", "INTO")) |
77 | 77 |
|
| 78 | +# single-occurrence clause keywords (at most one per SELECT scope) with no |
| 79 | +# identifier-collision risk - unlike GROUP/ORDER, which double as column names. |
| 80 | +# a repeat at the same paren-depth is the 'WHERE x WHERE y' structural bug (e.g. |
| 81 | +# a schema filter appended onto a base query that already carries a WHERE). |
| 82 | +_SINGLE_CLAUSE_KEYWORDS = frozenset(("WHERE", "HAVING")) |
| 83 | + |
| 84 | +# set operators that begin a fresh SELECT, resetting single-occurrence clauses at |
| 85 | +# the current scope ('a WHERE x UNION b WHERE y' is legal; two WHEREs are not). |
| 86 | +_SET_OPERATORS = frozenset(("UNION", "EXCEPT", "INTERSECT", "MINUS")) |
| 87 | + |
78 | 88 | # sqlmap's own templating markers. If any survives into a *final* outbound payload |
79 | 89 | # a substitution failed upstream (agent.py / cleanupPayload / queries.xml) - always |
80 | 90 | # a bug. Matched on the raw payload because a marker can leak anywhere (bare, inside |
@@ -309,6 +319,10 @@ def checkSanity(sql, keywords=None): |
309 | 319 | True |
310 | 320 | >>> bool(checkSanity("1UNION SELECT NULL")) |
311 | 321 | True |
| 322 | + >>> bool(checkSanity("SELECT a FROM t WHERE x=1 WHERE y=2")) |
| 323 | + True |
| 324 | + >>> checkSanity("SELECT a FROM t WHERE x=1 UNION SELECT b FROM u WHERE y=2") |
| 325 | + [] |
312 | 326 | """ |
313 | 327 | if not sql: |
314 | 328 | return [] |
@@ -419,4 +433,28 @@ def checkSanity(sql, keywords=None): |
419 | 433 | if cur.type == T_OTHER: |
420 | 434 | issues.append("stray character '%s' at offset %d" % (cur.value, cur.start)) |
421 | 435 |
|
| 436 | + # -- duplicated single-occurrence clause at one scope ('WHERE x WHERE y') -- |
| 437 | + # WHERE/HAVING may appear at most once per SELECT scope; a second one at the |
| 438 | + # same paren-depth (no set operator or ';' resetting the SELECT in between) |
| 439 | + # is a structural impossibility no surrounding query can undo - subquery |
| 440 | + # clauses live at a deeper depth and reset on '(' / ')'. |
| 441 | + scopeSeen = [set()] |
| 442 | + for token in sig: |
| 443 | + if token.type == T_LPAREN: |
| 444 | + scopeSeen.append(set()) |
| 445 | + elif token.type == T_RPAREN: |
| 446 | + if len(scopeSeen) > 1: |
| 447 | + scopeSeen.pop() |
| 448 | + elif token.type == T_SEMI: |
| 449 | + scopeSeen = [set()] |
| 450 | + elif token.type == T_KEYWORD: |
| 451 | + word = token.value.upper() |
| 452 | + if word in _SINGLE_CLAUSE_KEYWORDS: |
| 453 | + if word in scopeSeen[-1]: |
| 454 | + issues.append("duplicate '%s' clause at offset %d" % (word, token.start)) |
| 455 | + else: |
| 456 | + scopeSeen[-1].add(word) |
| 457 | + elif word in _SET_OPERATORS: |
| 458 | + scopeSeen[-1].clear() |
| 459 | + |
422 | 460 | return issues |
0 commit comments