sqlmap/tests/test_tamper.py
2026-06-15 09:50:47 +02:00

125 lines
5.3 KiB
Python

#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Tamper scripts (all ~70): contract, robustness on a payload battery, known
transforms, and documented fragile cases.
NOTE (flagged for author - real minor bugs surfaced by this suite):
* tamper/percentage.py raises UnboundLocalError on empty/None payload
(retVal is only assigned inside `if payload:`; missing `retVal = payload` init).
* tamper/escapequotes.py raises AttributeError on None payload (no guard).
68/70 tampers handle ""/None gracefully; these two are inconsistent. Pinned below
as KNOWN_FRAGILE so the suite stays green and a fix is a conscious change.
"""
import os
import glob
import importlib
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, ROOT
bootstrap()
from thirdparty import six
TAMPERS = sorted(os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py"))
if not f.endswith("__init__.py"))
# realistic, non-empty payloads (incl. unicode via escape, and a long one)
PAYLOADS = [
"1 AND 2=2",
"1 UNION SELECT NULL,NULL-- -",
"1 AND (SELECT 1 FROM dual)>0",
"1 AND '1'='1",
"admin'-- -",
u"1 AND name='caf\xe9'",
"1 AND " + "A" * 64, # modest "longer" payload
]
KNOWN_FRAGILE = set() # percentage/escapequotes empty/None crashes were FIXED by the author; now covered below
# Intentionally expensive by design (generates 4.2M parameters per call to flood Lua-Nginx
# WAFs) -> ~6s/call. NOT a bug; excluded from execution to keep the unit suite fast.
HEAVY = {"luanginxmore"}
class TestTamperRobustness(unittest.TestCase):
def test_no_crash_returns_string(self):
for name in TAMPERS:
if name in HEAVY:
continue
mod = importlib.import_module("tamper.%s" % name)
for p in PAYLOADS:
try:
r = mod.tamper(p)
except Exception as ex:
self.fail("tamper '%s' crashed on %r: %s" % (name, p[:25], ex))
self.assertTrue(isinstance(r, six.string_types),
msg="tamper '%s' returned %s for %r" % (name, type(r).__name__, p[:25]))
class TestTamperEmptyNoneHandling(unittest.TestCase):
def test_graceful_on_empty_and_none(self):
for name in TAMPERS:
if name in KNOWN_FRAGILE or name in HEAVY:
continue
mod = importlib.import_module("tamper.%s" % name)
for p in ("", None):
try:
mod.tamper(p)
except Exception as ex:
self.fail("tamper '%s' crashed on %r: %s" % (name, p, ex))
def test_previously_fragile_now_fixed(self):
# regression pin: percentage/escapequotes used to crash on empty/None; now must be graceful
import tamper.percentage as _p
import tamper.escapequotes as _e
self.assertEqual(_p.tamper(""), "")
self.assertIsNone(_p.tamper(None))
self.assertEqual(_e.tamper(""), "")
self.assertIsNone(_e.tamper(None))
class TestKnownTransforms(unittest.TestCase):
# authoritative input->output taken from each tamper's own doctest
CASES = {
"space2comment": ("SELECT id FROM users", "SELECT/**/id/**/FROM/**/users"),
"between": ("1 AND A > B--", "1 AND A NOT BETWEEN 0 AND B--"),
"charencode": ("SELECT FIELD FROM%20TABLE",
"%53%45%4C%45%43%54%20%46%49%45%4C%44%20%46%52%4F%4D%20%54%41%42%4C%45"),
"apostrophemask": ("1 AND '1'='1", "1 AND %EF%BC%871%EF%BC%87=%EF%BC%871"),
"equaltolike": ("SELECT * FROM users WHERE id=1", "SELECT * FROM users WHERE id LIKE 1"),
"percentage": ("SELECT FIELD FROM TABLE", "%S%E%L%E%C%T %F%I%E%L%D %F%R%O%M %T%A%B%L%E"),
# additional deterministic transforms (verified stable across repeated calls)
"space2plus": ("1 AND 2>1", "1+AND+2>1"),
"unionalltounion": ("1 UNION ALL SELECT 2", "1 UNION SELECT 2"),
"halfversionedmorekeywords": ("1 AND 2>1", "1/*!0AND 2>1"),
"versionedkeywords": ("1 AND 2>1", "1/*!AND*/2>1"),
"appendnullbyte": ("1", "1%00"),
"base64encode": ("1 AND 1=1", "MSBBTkQgMT0x"),
"greatest": ("1 AND A>B", "1 AND GREATEST(A,B+1)=A"),
"ifnull2ifisnull": ("IFNULL(a,b)", "IF(ISNULL(a),b,a)"),
"symboliclogical": ("1 AND 2 OR 3", "1 %26%26 2 %7C%7C 3"),
"bluecoat": ("1 AND 2=2", "1 AND%092 LIKE 2"),
"apostrophenullencode": ("'", "%00%27"),
}
def test_transforms(self):
for name, (inp, expected) in self.CASES.items():
mod = importlib.import_module("tamper.%s" % name)
self.assertEqual(mod.tamper(inp), expected, msg="tamper '%s'(%r)" % (name, inp))
class TestTamperCount(unittest.TestCase):
def test_expected_count(self):
# there are currently 70 tamper scripts; floor at 70 so an accidental deletion (or a glob
# that silently stops matching) fails loudly rather than passing on a shrunken set
self.assertGreaterEqual(len(TAMPERS), 70, msg="expected >=70 tampers, found %d" % len(TAMPERS))
if __name__ == "__main__":
unittest.main(verbosity=2)