sqlmap/tests/test_agent_dialects.py
Miroslav Štampar cb20a446ae
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
Update of unit tests
2026-06-28 14:28:42 +02:00

274 lines
11 KiB
Python

#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Cross-dialect exercise of lib/core/agent.py payload-assembly helpers.
agent.py builds SQL payloads from per-DBMS dialect templates (queries.xml).
The helpers are pure given the identified back-end DBMS, so driving each one
across EVERY supported dialect walks the dialect-specific branches (CAST forms,
concatenation operators, LIMIT/TOP/ROWNUM shapes, ...) without a live target.
These are smoke-level assertions (right type, dialect tokens present) rather than
golden strings: the goal is to traverse the dialect branches the single-DBMS
tests in test_agent.py do not reach.
"""
import os
import re
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
bootstrap()
from lib.core.agent import agent
from lib.core.data import queries
DIALECTS = sorted(queries.keys())
# --------------------------------------------------------------------------- #
# Per-dialect expectation maps (keyed by the DBMS display name == queries key).
#
# These were derived by inspecting the actual agent.py output for every dialect
# (the queries.xml templates drive the branches). They pin the *distinctive*
# dialect token so an assertion fails if the dialect branch collapses to the
# wrong form (e.g. concat operator swapped, null-wrapper dropped).
# --------------------------------------------------------------------------- #
# concatQuery / simpleConcatenate join operator per dialect.
CONCAT_OPERATOR = {
"ClickHouse": "CONCAT(",
"Informix": "CONCAT(",
"MySQL": "CONCAT(",
"SAP MaxDB": "CONCAT(",
"Microsoft SQL Server": "+",
"Sybase": "+",
"Microsoft Access": "&",
}
# everything not listed above uses the SQL standard "||"
CONCAT_OPERATOR_DEFAULT = "||"
# nullAndCastField / nullCastConcatFields NULL-wrapper function per dialect.
NULL_WRAPPER = {
"Altibase": "NVL",
"Apache Derby": "COALESCE",
"ClickHouse": "ifNull",
"CrateDB": "COALESCE",
"Cubrid": "IFNULL",
"Firebird": "COALESCE",
"FrontBase": "COALESCE",
"H2": "IFNULL",
"HSQLDB": "IFNULL",
"IBM DB2": "COALESCE",
"Informix": "NVL",
"InterSystems Cache": "COALESCE",
"Mckoi": "IF(",
"Microsoft Access": "IIF",
"Microsoft SQL Server": "ISNULL",
"MimerSQL": "COALESCE",
"MonetDB": "COALESCE",
"MySQL": "IFNULL",
"Oracle": "NVL",
"PostgreSQL": "COALESCE",
"Presto": "COALESCE",
"Raima Database Manager": "IFNULL",
"SAP MaxDB": "VALUE",
"SQLite": "COALESCE",
"Snowflake": "NVL",
"Spanner": "IFNULL",
"Sybase": "ISNULL",
"Vertica": "COALESCE",
"Virtuoso": "__MAX_NOTNULL",
"eXtremeDB": "IFNULL",
}
# hexConvertField: dialects that DO have a hex function, mapped to its token.
HEX_FUNCTION = {
"Altibase": "HEX_ENCODE(",
"Cubrid": "HEX(",
"H2": "RAWTOHEX(",
"IBM DB2": "HEX(",
"Microsoft SQL Server": "fn_varbintohexstr",
"MySQL": "HEX(",
"Oracle": "RAWTOHEX(",
"PostgreSQL": "ENCODE(",
"Presto": "TO_HEX(",
"SAP MaxDB": "HEX(",
"SQLite": "HEX(",
"Spanner": "TO_HEX(",
"Sybase": "BINTOSTR",
"Vertica": "TO_HEX(",
}
# dialects that intentionally do NOT support hex conversion and return the
# field unchanged (a no-op the old "colname in out" check silently masked).
HEX_NOOP = set(DIALECTS) - set(HEX_FUNCTION)
# limitQuery: dialects whose limit template is empty so the call legitimately
# raises (no .limit.query). These are skipped by name in the limit-token test.
LIMIT_RAISES = {"Mckoi", "Raima Database Manager"}
# dialects with no special limitQuery branch: the query is returned unchanged
# (no limit token is emitted).
LIMIT_PASSTHROUGH = {"Informix", "Microsoft Access", "SAP MaxDB"}
# broad set of dialect limit tokens; every running, non-passthrough dialect
# emits at least one of these.
LIMIT_TOKENS = ("LIMIT", "TOP", "ROWNUM", "FETCH", "ROWS", "OFFSET", "ROW_NUMBER")
class TestNullCastConcatFields(unittest.TestCase):
def test_all_dialects(self):
for dbms in DIALECTS:
set_dbms(dbms)
out = agent.nullCastConcatFields("user,password")
self.assertIsInstance(out, str, msg=dbms)
# both column names survive the null/cast/concat rewrite
self.assertIn("user", out, msg=dbms)
self.assertIn("password", out, msg=dbms)
# the dialect-specific NULL-wrapper must be present (the column-name
# check above is always satisfied and so cannot catch a broken
# branch); this fails if the wrapper collapses to the wrong form.
self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out))
def test_literal_passthrough(self):
for dbms in DIALECTS:
set_dbms(dbms)
# a bare quoted literal is returned untouched
self.assertEqual(agent.nullCastConcatFields("'abc'"), "'abc'", msg=dbms)
class TestNullAndCastField(unittest.TestCase):
def test_all_dialects(self):
for dbms in DIALECTS:
set_dbms(dbms)
out = agent.nullAndCastField("colname")
self.assertIsInstance(out, str, msg=dbms)
self.assertIn("colname", out, msg=dbms)
# dialect-specific NULL wrapper (IFNULL/COALESCE/NVL/ISNULL/IIF/...)
self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out))
class TestHexConvertField(unittest.TestCase):
def test_all_dialects(self):
for dbms in DIALECTS:
set_dbms(dbms)
out = agent.hexConvertField("colname")
self.assertIsInstance(out, str, msg=dbms)
self.assertIn("colname", out, msg=dbms)
if dbms in HEX_FUNCTION:
# the dialect's hex function wraps the field
self.assertIn(HEX_FUNCTION[dbms], out, msg="%s: %s" % (dbms, out))
else:
# intentional no-op: the field is returned verbatim. The old
# "colname in out" check masked this; pin the exact identity.
self.assertEqual(out, "colname", msg="%s expected no-op: %s" % (dbms, out))
class TestConcatQuery(unittest.TestCase):
def test_all_dialects(self):
for dbms in DIALECTS:
set_dbms(dbms)
out = agent.concatQuery("SELECT user FROM users")
self.assertIsInstance(out, str, msg=dbms)
# concatQuery output is dialect-specific: MySQL/ClickHouse/Informix/
# SAP MaxDB use CONCAT(...), MSSQL/Sybase use +, Access uses &, and
# the rest use the SQL-standard ||. Assert the right operator so the
# test fails if the dialect collapses to the wrong concatenation.
expected = CONCAT_OPERATOR.get(dbms, CONCAT_OPERATOR_DEFAULT)
self.assertIn(expected, out, msg="%s: %s" % (dbms, out))
class TestSimpleConcatenate(unittest.TestCase):
def test_all_dialects(self):
for dbms in DIALECTS:
set_dbms(dbms)
out = agent.simpleConcatenate("a", "b")
self.assertIsInstance(out, str, msg=dbms)
self.assertIn("a", out, msg=dbms)
self.assertIn("b", out, msg=dbms)
class TestForgeUnionQuery(unittest.TestCase):
def test_all_dialects(self):
for dbms in DIALECTS:
set_dbms(dbms)
count = 3
out = agent.forgeUnionQuery("SELECT user FROM users", -1, count, None,
None, None, "NULL", None)
self.assertIsInstance(out, str, msg=dbms)
self.assertIn("UNION", out.upper(), msg=dbms)
# position -1 with char NULL fills every one of the `count` columns
# with the char, so the NULL char must appear exactly `count` times.
# (a hardcoded "UNION in out" check could not catch a wrong column
# count.) Match NULL as a whole token to avoid matching substrings.
self.assertEqual(re.findall(r"\bNULL\b", out).__len__(), count,
msg="%s expected %d NULLs: %s" % (dbms, count, out))
class TestLimitQuery(unittest.TestCase):
def test_all_dialects(self):
for dbms in DIALECTS:
set_dbms(dbms)
# Only Mckoi/Raima have an empty limit template and legitimately
# raise; skip exactly those by name rather than swallowing *any*
# exception (which would hide a real regression in another dialect).
if dbms in LIMIT_RAISES:
with self.assertRaises(Exception, msg=dbms):
agent.limitQuery(0, "SELECT user FROM users", "user")
continue
out = agent.limitQuery(0, "SELECT user FROM users", "user")
self.assertIsInstance(out, str, msg=dbms)
if dbms in LIMIT_PASSTHROUGH:
# these dialects have no dedicated limitQuery branch and return
# the query unchanged (documented no-op).
self.assertEqual(out, "SELECT user FROM users", msg=dbms)
else:
# every other running dialect emits a real limit construct
self.assertTrue(any(tok in out.upper() for tok in LIMIT_TOKENS),
msg="%s missing limit token: %s" % (dbms, out))
class TestForgeCaseStatement(unittest.TestCase):
def test_all_dialects(self):
for dbms in DIALECTS:
set_dbms(dbms)
out = agent.forgeCaseStatement("1=1")
self.assertIsInstance(out, str, msg=dbms)
# dialects vary on the conditional form (CASE / IIF / IF); the
# condition itself is always embedded
self.assertIn("1=1", out, msg=dbms)
# ...but the conditional construct itself must also be present,
# otherwise the "1=1" check alone could pass on a degenerate output.
self.assertTrue("CASE" in out or "IIF" in out or "IF(" in out,
msg="%s missing conditional construct: %s" % (dbms, out))
class TestPrefixSuffixAcrossDialects(unittest.TestCase):
def test_prefix_suffix(self):
for dbms in DIALECTS:
set_dbms(dbms)
prefix = agent.prefixQuery("1=1")
suffix = agent.suffixQuery("1=1")
self.assertIsInstance(prefix, str, msg=dbms)
self.assertIsInstance(suffix, str, msg=dbms)
# prefixQuery pads a leading space ahead of the expression by default
self.assertEqual(prefix, " 1=1", msg="%s prefix: %r" % (dbms, prefix))
# suffixQuery returns the expression itself (no extra clause/comment)
self.assertEqual(suffix, "1=1", msg="%s suffix: %r" % (dbms, suffix))
class TestRunAsDBMSUserAndWhere(unittest.TestCase):
def test_run_as_user_noop_without_conf(self):
for dbms in DIALECTS:
set_dbms(dbms)
# without conf.dbmsCred the query is returned unchanged
self.assertEqual(agent.runAsDBMSUser("SELECT 1"), "SELECT 1", msg=dbms)
if __name__ == "__main__":
unittest.main()