From 0338c13063595087967a2b9a2986ac91ab611e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 20:27:37 +0200 Subject: [PATCH] Minor update for SQLlinter --- lib/core/settings.py | 2 +- lib/utils/sqllint.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_sqllint.py | 6 ++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 9cd7d6a84..b1a69bbd8 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.101" +VERSION = "1.10.7.102" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/utils/sqllint.py b/lib/utils/sqllint.py index 867b4c7d2..f38ef2858 100644 --- a/lib/utils/sqllint.py +++ b/lib/utils/sqllint.py @@ -75,6 +75,16 @@ _BINARY_SYMBOLS = frozenset(("=", "<>", "!=", "<", ">", "<=", ">=", "/", "%", "| # "a,limit,b" would false-positive. _CLAUSE_KEYWORDS = frozenset(("FROM", "WHERE", "HAVING", "INTO")) +# single-occurrence clause keywords (at most one per SELECT scope) with no +# identifier-collision risk - unlike GROUP/ORDER, which double as column names. +# a repeat at the same paren-depth is the 'WHERE x WHERE y' structural bug (e.g. +# a schema filter appended onto a base query that already carries a WHERE). +_SINGLE_CLAUSE_KEYWORDS = frozenset(("WHERE", "HAVING")) + +# set operators that begin a fresh SELECT, resetting single-occurrence clauses at +# the current scope ('a WHERE x UNION b WHERE y' is legal; two WHEREs are not). +_SET_OPERATORS = frozenset(("UNION", "EXCEPT", "INTERSECT", "MINUS")) + # sqlmap's own templating markers. If any survives into a *final* outbound payload # a substitution failed upstream (agent.py / cleanupPayload / queries.xml) - always # a bug. Matched on the raw payload because a marker can leak anywhere (bare, inside @@ -309,6 +319,10 @@ def checkSanity(sql, keywords=None): True >>> bool(checkSanity("1UNION SELECT NULL")) True + >>> bool(checkSanity("SELECT a FROM t WHERE x=1 WHERE y=2")) + True + >>> checkSanity("SELECT a FROM t WHERE x=1 UNION SELECT b FROM u WHERE y=2") + [] """ if not sql: return [] @@ -419,4 +433,28 @@ def checkSanity(sql, keywords=None): if cur.type == T_OTHER: issues.append("stray character '%s' at offset %d" % (cur.value, cur.start)) + # -- duplicated single-occurrence clause at one scope ('WHERE x WHERE y') -- + # WHERE/HAVING may appear at most once per SELECT scope; a second one at the + # same paren-depth (no set operator or ';' resetting the SELECT in between) + # is a structural impossibility no surrounding query can undo - subquery + # clauses live at a deeper depth and reset on '(' / ')'. + scopeSeen = [set()] + for token in sig: + if token.type == T_LPAREN: + scopeSeen.append(set()) + elif token.type == T_RPAREN: + if len(scopeSeen) > 1: + scopeSeen.pop() + elif token.type == T_SEMI: + scopeSeen = [set()] + elif token.type == T_KEYWORD: + word = token.value.upper() + if word in _SINGLE_CLAUSE_KEYWORDS: + if word in scopeSeen[-1]: + issues.append("duplicate '%s' clause at offset %d" % (word, token.start)) + else: + scopeSeen[-1].add(word) + elif word in _SET_OPERATORS: + scopeSeen[-1].clear() + return issues diff --git a/tests/test_sqllint.py b/tests/test_sqllint.py index 63f409ff1..4c06bbcde 100644 --- a/tests/test_sqllint.py +++ b/tests/test_sqllint.py @@ -66,6 +66,9 @@ DIALECT_GOOD = ( "SELECT LIMIT 0 1 DISTINCT(user) FROM INFORMATION_SCHEMA.SYSTEM_USERS", # HSQLDB space-LIMIT "SELECT TOP 1 name FROM master..sysdatabases", # MSSQL TOP + db..table "CONCAT('\\',0x71,(SELECT 1))", # backslash-literal string (ANSI) + "SELECT a FROM t WHERE x=1 UNION SELECT b FROM u WHERE y=2", # two WHEREs across UNION (legal) + "SELECT a FROM (SELECT b FROM t WHERE c=1) z WHERE d=2", # subquery WHERE + outer WHERE (different scopes) + "SELECT a FROM t WHERE x IN (SELECT c FROM d WHERE e=1) AND f=2", # WHERE with a WHERE'd subquery ) # malformed fragments/statements that MUST flag @@ -92,6 +95,9 @@ BAD = ( "1 UNION ALLSELECT NULL", # glued keyword after UNION "1 AND ORD(MID((SELECT COUNT(x FROM t),1,1))>64", # unbalanced parentheses (dropped ')') "1 UNION ALL SELECT ,CONCAT(0x71,a,0x71) FROM t", # comma right after SELECT + "SELECT OWNER,OBJECT_NAME FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE IN ('TABLE','VIEW') WHERE OWNER IN ('APPU')", # double WHERE (mis-appended schema filter) + "SELECT tabschema,tabname FROM syscat.tables WHERE type IN ('T','V') WHERE tabschema IN ('DB2INST1')", # double WHERE (DB2) + "SELECT a FROM t WHERE x=1 HAVING c>1 HAVING d<2", # duplicate HAVING ) # cross-dialect valid constructs the near-keyword / comma / digit-glue rules must