Adding unit tests

This commit is contained in:
Miroslav Štampar 2026-06-15 09:50:47 +02:00
parent 48b915b5ee
commit 3816df1241
39 changed files with 3501 additions and 2 deletions

View file

@ -40,6 +40,9 @@ jobs:
- name: Basic import test
run: python -c "import sqlmap; import sqlmapapi"
- name: Unit tests
run: python -m unittest discover -s tests -p "test_*.py"
- name: Smoke test
run: python sqlmap.py --smoke

View file

@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch
48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
b0e5477bbbf2eb673fa6b99829c2f51e108bebd3f572d0527e90684c157ba3c6 lib/core/settings.py
a910686c6eba592ba3f6fc5cbb8bed1bd6c330b0165c7c5dc927a71c5ae8be88 lib/core/settings.py
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
@ -564,6 +564,42 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional
7afc4d262b97773e67dcfa3e253a9a060dc964750f01d739636d17ee069f1512 tamper/versionedkeywords.py
0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py
ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py
44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py
bfb553602eb5d20b4ab5928dbcf8e6a3e7e5ff69f7d30d1f53ef6d323c237f6c tests/test_agent.py
d4d7d3525d25ce72bf38bd38b5fdf61144e381993d63be7dc72b2b4811ffab67 tests/test_bigarray.py
27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py
9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py
a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_common_helpers.py
7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py
8593f14a18c4445c58b2e59462adcb761074ac7217cd7c3808519a90ba279bda tests/test_convert.py
5016119bdb57094381afdca35ef29a4a6641e26e4b48a9119f1db633e6123d29 tests/test_datafiles.py
9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py
bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py
8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py
c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py
205e84827461101a78b2cffaa3de49795a1214e92276fc7fd40f3456657062b9 tests/test_identifiers_output.py
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py
5c95e7863190e440234f231864fb1219c35207132762858cc95181c57086bafc tests/test_replication.py
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py
a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py
d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py
41907c873663401f979b87eaff3efc8d52e0ce96cbe1eef7aa70c6d3af8cd5cf tests/test_strings.py
f3a628db8a3e05baee580c02132e95b164695e4b3ee1785707e3ea148702449a tests/test_tamper.py
b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py
639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py
708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py
4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py
4f095ebda1b9bddde082ed464e863400cf23e9bf26f081948706213b35069195 tests/_testutils.py
2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.6.105"
VERSION = "1.10.6.106"
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)

6
tests/__init__.py Normal file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""

89
tests/_testutils.py Normal file
View file

@ -0,0 +1,89 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Shared bootstrap for the sqlmap unit/regression test suite.
Brings sqlmap's global state (conf/kb, the 'reversible' codec, cross-references,
option defaults) up far enough that pure/near-pure library functions can be
exercised in isolation - WITHOUT a live target, network, or DBMS.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import os
import sys
import warnings
# Quieten import-time noise before any sqlmap/3rd-party module is imported by bootstrap():
# e.g. cryptography's "Python 2 is no longer supported" CryptographyDeprecationWarning via pymysql.
warnings.filterwarnings("ignore", message=".*Python 2 is no longer supported.*")
warnings.filterwarnings("ignore", category=DeprecationWarning)
# sqlmap reconfigures stdout at startup; py3 emits a benign RuntimeWarning about line buffering
warnings.filterwarnings("ignore", message=".*line buffering.*binary mode.*")
_BOOTSTRAPPED = False
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def bootstrap():
"""Idempotently initialize sqlmap global state for testing."""
global _BOOTSTRAPPED
if _BOOTSTRAPPED:
return
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
# a dummy target so cmdLineParser() populates ALL option defaults without erroring;
# save/restore the real argv so the unittest runner isn't confused by it
_orig_argv = list(sys.argv)
sys.argv = ["sqlmap.py", "-u", "http://test.invalid/?id=1"]
from lib.core.common import setPaths
from lib.core.patch import dirtyPatches, resolveCrossReferences
setPaths(ROOT)
dirtyPatches() # registers the 'reversible' codec error handler, etc.
resolveCrossReferences()
from lib.core.option import _setConfAttributes, _setKnowledgeBaseAttributes, _loadQueries
_setConfAttributes()
_setKnowledgeBaseAttributes()
_loadQueries() # populate the `queries` dict from queries.xml (needed by dialect builders)
from lib.core.data import conf, kb
from lib.core.defaults import defaults
from lib.parse.cmdline import cmdLineParser
args = cmdLineParser()
parsed = args.__dict__ if hasattr(args, "__dict__") else dict(args)
for k, v in parsed.items():
conf[k] = v
# overlay canonical defaults for options left None (sqlmap does this during init)
for k, v in defaults.items():
if conf.get(k) is None:
conf[k] = v
kb.binaryField = False # normally set lazily during extraction
# Silence sqlmap's application logger - tests assert on results, not log output, and the
# INFO/WARNING/ERROR chatter (column counts, reflective-value notices, an intentionally
# malformed-deflate error, etc.) just clutters the unittest report.
import logging
logging.getLogger("sqlmapLog").setLevel(logging.CRITICAL + 1)
sys.argv = _orig_argv # restore so unittest's arg parsing works
_BOOTSTRAPPED = True
def set_dbms(name):
"""Force the identified back-end DBMS for dialect-dependent functions.
Uses forceDbms (not setDbms) so switching DBMS repeatedly in one process does
not trigger the interactive fingerprint-mismatch prompt.
"""
from lib.core.common import Backend
from lib.core.data import kb
kb.stickyDBMS = False
Backend.forceDbms(name)

86
tests/test_agent.py Normal file
View file

@ -0,0 +1,86 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Payload assembly helpers in lib/core/agent.py.
These are the (mostly) DBMS-independent string transforms that wrap, fold and
clean a payload on its way to the wire: prefix/suffix, payload delimiters,
field extraction, CONCAT folding, and RAND-marker cleanup. All values below
were probed from real output, not assumed.
"""
import os
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.enums import DBMS
from lib.core.settings import PAYLOAD_DELIMITER
class TestPayloadDelimiters(unittest.TestCase):
def test_add(self):
self.assertEqual(agent.addPayloadDelimiters("1 AND 1=1"),
"%s1 AND 1=1%s" % (PAYLOAD_DELIMITER, PAYLOAD_DELIMITER))
def test_remove(self):
wrapped = "%spayload%s" % (PAYLOAD_DELIMITER, PAYLOAD_DELIMITER)
self.assertEqual(agent.removePayloadDelimiters(wrapped), "payload")
def test_remove_none_is_none(self):
self.assertIsNone(agent.removePayloadDelimiters(None))
def test_roundtrip(self):
for p in ["1=1", "1 AND SLEEP(5)", "' OR '1'='1", "", "a%sb" % "x"]:
self.assertEqual(agent.removePayloadDelimiters(agent.addPayloadDelimiters(p)), p,
msg="delimiter round-trip for %r" % p)
class TestPrefixSuffix(unittest.TestCase):
def test_prefix_default_pads_space(self):
# with no configured prefix, a single leading space is prepended
self.assertEqual(agent.prefixQuery("1=1"), " 1=1")
def test_suffix_default_identity(self):
self.assertEqual(agent.suffixQuery("1=1"), "1=1")
class TestGetFields(unittest.TestCase):
def test_extracts_select_list(self):
# getFields(query) returns an 8-tuple; the fields-bearing slots are:
# [0],[1] = regex match objects for the SELECT/expression (must be found, not None)
# [5] = parsed field list, [6] = raw fields string
# (asserting the match objects guards against a refactor that silently shifts the tuple)
result = agent.getFields("SELECT a,b FROM t")
self.assertIsNotNone(result[0], msg="getFields did not match the SELECT")
self.assertEqual(result[5], ["a", "b"])
self.assertEqual(result[6], "a,b")
class TestConcatQuery(unittest.TestCase):
def test_mysql_concat_folding(self):
set_dbms(DBMS.MYSQL)
q = agent.concatQuery("SELECT a FROM t")
# folds the field through CONCAT with the start/stop delimiters and keeps the FROM
self.assertTrue(q.startswith("CONCAT("), msg=q)
self.assertIn("IFNULL(CAST(a AS NCHAR),' ')", q)
self.assertTrue(q.endswith("FROM t"), msg=q)
class TestCleanupPayload(unittest.TestCase):
def test_randnum_marker_replaced_with_digits(self):
out = agent.cleanupPayload("SELECT [RANDNUM]")
self.assertNotIn("[RANDNUM]", out, msg="marker not replaced: %r" % out) # actually substituted
self.assertTrue(out.startswith("SELECT "), msg=out)
self.assertTrue(out.split()[-1].isdigit(), msg=out) # ...and replaced with a concrete number
if __name__ == "__main__":
unittest.main(verbosity=2)

95
tests/test_bigarray.py Normal file
View file

@ -0,0 +1,95 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
BigArray disk-spill semantics (lib/core/bigarray.py).
BigArray is the structure that lets sqlmap dump tables far larger than RAM: once
the in-memory chunk exceeds chunk_size it is pickled to a temp file and a new
chunk starts. The tricky, easy-to-break part is that indexing / iteration /
pop / pickling must stay correct ACROSS the in-memory<->on-disk boundary.
These force a spill with a tiny chunk_size and assert the data survives intact.
"""
import os
import pickle
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.bigarray import BigArray
N = 5000
def _make_spilled():
# tiny chunk_size guarantees many on-disk chunks for N items
ba = BigArray(chunk_size=1024)
for i in range(N):
ba.append("item-%d" % i)
return ba
class TestSpill(unittest.TestCase):
def test_actually_spilled_to_disk(self):
ba = _make_spilled()
self.assertGreater(len(ba.chunks), 1, msg="expected multiple chunks (a disk spill)")
# stronger than "more than one chunk": at least one chunk must be a real on-disk file
# (spilled chunks are stored as filenames). Otherwise this could pass while everything
# stayed in RAM.
disk_chunks = [c for c in ba.chunks if isinstance(c, str)]
self.assertTrue(disk_chunks, msg="no chunk was spilled to disk")
self.assertTrue(os.path.exists(disk_chunks[0]), msg="spilled chunk file missing on disk")
def test_len(self):
self.assertEqual(len(_make_spilled()), N)
def test_random_access_across_boundary(self):
ba = _make_spilled()
for i in (0, 1, 499, 500, 2500, N - 1):
self.assertEqual(ba[i], "item-%d" % i, msg="ba[%d]" % i)
def test_negative_index(self):
ba = _make_spilled()
self.assertEqual(ba[-1], "item-%d" % (N - 1))
def test_iteration_order_preserved(self):
ba = _make_spilled()
for idx, value in enumerate(ba):
if value != "item-%d" % idx:
self.fail("iteration order broke at %d: %r" % (idx, value))
self.assertEqual(idx, N - 1)
def test_pop_from_end(self):
ba = _make_spilled()
self.assertEqual(ba.pop(), "item-%d" % (N - 1))
self.assertEqual(len(ba), N - 1)
def test_pickle_roundtrip_across_spill(self):
ba = _make_spilled()
restored = pickle.loads(pickle.dumps(ba))
self.assertIsInstance(restored, BigArray)
self.assertEqual(len(restored), N)
self.assertEqual(restored[0], "item-0")
self.assertEqual(restored[N - 1], "item-%d" % (N - 1))
class TestInMemorySmall(unittest.TestCase):
def test_no_spill_for_small(self):
ba = BigArray([1, 2, 3])
self.assertEqual(len(ba), 3)
self.assertEqual(list(ba), [1, 2, 3])
# the actual point of this test (the name promised it): a tiny array stays in ONE
# in-memory chunk and never touches disk
self.assertEqual(len(ba.chunks), 1, msg="small array unexpectedly spilled: %r" % (ba.chunks,))
self.assertFalse(any(isinstance(c, str) for c in ba.chunks), msg="small array wrote a disk chunk")
if __name__ == "__main__":
unittest.main(verbosity=2)

71
tests/test_charset.py Normal file
View file

@ -0,0 +1,71 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Response charset / meta detection and parameter parsing.
checkCharEncoding canonicalizes the encoding sqlmap will decode a page with;
META_CHARSET_REGEX / HTML_TITLE_REGEX / META_REFRESH_REGEX pull structural hints
out of the body; paramToDict splits the parameters sqlmap will inject into.
These feed decodePage and the comparison engine, so the canonical/None results
are pinned here.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.request.basic import checkCharEncoding
from lib.core.common import extractRegexResult, paramToDict
from lib.core.enums import PLACE
from lib.core.settings import META_CHARSET_REGEX, HTML_TITLE_REGEX, META_REFRESH_REGEX
class TestCheckCharEncoding(unittest.TestCase):
def test_canonical_known(self):
for enc in ("utf-8", "windows-1252", "iso-8859-1", "ascii", "latin1"):
self.assertEqual(checkCharEncoding(enc, False), enc, msg="checkCharEncoding(%r)" % enc)
def test_normalizes_aliases(self):
self.assertEqual(checkCharEncoding("UTF8", False), "utf8")
self.assertEqual(checkCharEncoding("us-ascii", False), "ascii")
def test_unknown_is_none(self):
self.assertIsNone(checkCharEncoding("boguscharset123", False))
def test_none_is_none(self):
self.assertIsNone(checkCharEncoding(None, False))
class TestBodyHints(unittest.TestCase):
def test_meta_charset(self):
self.assertEqual(extractRegexResult(META_CHARSET_REGEX, '<head><meta charset="utf-8"></head>'), "utf-8")
def test_title(self):
self.assertEqual(extractRegexResult(HTML_TITLE_REGEX, "<title>Login Page</title>"), "Login Page")
def test_meta_refresh_url(self):
self.assertEqual(extractRegexResult(META_REFRESH_REGEX,
'<meta http-equiv="refresh" content="0; url=/next">'), "/next")
def test_no_match_is_none(self):
self.assertIsNone(extractRegexResult(HTML_TITLE_REGEX, "<body>no title here</body>"))
class TestParamToDict(unittest.TestCase):
# NOTE: GET parsing is covered in test_urls.py; here we only cover the COOKIE place,
# which uses a different (semicolon) delimiter and is a distinct code path.
def test_cookie_semicolon_delimited(self):
d = paramToDict(PLACE.COOKIE, "sid=abc; theme=dark")
self.assertEqual(d.get("sid"), "abc")
self.assertEqual(d.get("theme"), "dark")
if __name__ == "__main__":
unittest.main(verbosity=2)

67
tests/test_cloak.py Normal file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
cloak / decloak (extra/cloak/cloak.py) - the zlib+XOR transform used to pack the
payload stager files (.py_) that sqlmap drops and unpacks on a target during
takeover/file-write. A broken round-trip here corrupts every deployed stager.
decloak(cloak(x)) must be the identity for arbitrary bytes; pinned with known
vectors and a property sweep over random binary inputs.
"""
import os
import random
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
# cloak ships under extra/cloak (build-time + runtime stager packer)
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "extra", "cloak"))
import cloak as C
RND = random.Random(1234)
def _rand_bytes(n):
return bytes(bytearray(RND.randint(0, 255) for _ in range(n)))
class TestCloakRoundTrip(unittest.TestCase):
def test_known_payload(self):
data = b"print('stager')"
self.assertEqual(C.decloak(data=C.cloak(data=data)), data)
def test_empty(self):
self.assertEqual(C.decloak(data=C.cloak(data=b"")), b"")
def test_cloak_changes_bytes(self):
# cloak must actually transform (compress+xor), not pass through
data = b"A" * 64
self.assertNotEqual(C.cloak(data=data), data)
def test_cloak_compresses_compressible_input(self):
# highly-repetitive input must come out SMALLER (proves zlib is actually applied,
# not just an XOR-only obfuscation). NOTE: random/incompressible data would grow,
# so this assertion is only valid for compressible input.
data = b"A" * 1000
self.assertLess(len(C.cloak(data=data)), len(data))
def test_property_random_binary(self):
for _ in range(500):
data = _rand_bytes(RND.randint(0, 200))
self.assertEqual(C.decloak(data=C.cloak(data=data)), data, msg="cloak round-trip failed for %r" % data)
def test_property_large(self):
for size in (1024, 8192, 65536):
data = _rand_bytes(size)
self.assertEqual(C.decloak(data=C.cloak(data=data)), data, msg="cloak round-trip failed at size %d" % size)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -0,0 +1,76 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Assorted request-shaping helpers in lib/core/common.py:
chunkSplitPostData (HTTP chunked-transfer evasion), randomizeParameterValue
(tamper/cache-buster), getHostHeader (Host header derivation).
chunkSplitPostData uses random chunk sizes, so its output is asserted
structurally (reassembles to the original, terminates correctly) rather than
byte-for-byte; randomizeParameterValue is asserted via its invariants.
"""
import os
import re
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import chunkSplitPostData, randomizeParameterValue, getHostHeader
def _dechunk(data):
"""Reassemble an HTTP/1.1 chunked body back into its payload."""
out = []
i = 0
while i < len(data):
nl = data.index("\r\n", i)
size = int(data[i:nl].split(";")[0], 16) # size; optional chunk-extension
start = nl + 2
out.append(data[start:start + size])
i = start + size + 2 # skip chunk data + trailing CRLF
if size == 0:
break
return "".join(out)
class TestChunkSplit(unittest.TestCase):
def test_reassembles_to_original(self):
for payload in ("a=1&b=2", "x" * 50, "single=value", ""):
self.assertEqual(_dechunk(chunkSplitPostData(payload)), payload,
msg="chunk reassembly failed for %r" % payload)
def test_terminates_with_zero_chunk(self):
self.assertTrue(chunkSplitPostData("a=1&b=2").endswith("0\r\n\r\n"))
class TestRandomizeParameterValue(unittest.TestCase):
def test_length_preserved(self):
for v in ("abc123", "value", "42", "MixedCASE99"):
self.assertEqual(len(randomizeParameterValue(v)), len(v), msg="length changed for %r" % v)
def test_char_class_preserved(self):
# letters stay letters, digits stay digits (positionally)
src = "abc123XYZ789"
out = randomizeParameterValue(src)
for a, b in zip(src, out):
self.assertEqual(a.isdigit(), b.isdigit(), msg="char class changed: %r -> %r" % (a, b))
self.assertEqual(a.isalpha(), b.isalpha(), msg="char class changed: %r -> %r" % (a, b))
class TestGetHostHeader(unittest.TestCase):
def test_with_port(self):
self.assertEqual(getHostHeader("http://h:8080/p"), "h:8080")
def test_without_port(self):
self.assertEqual(getHostHeader("http://example.com/path"), "example.com")
if __name__ == "__main__":
unittest.main(verbosity=2)

132
tests/test_comparison.py Normal file
View file

@ -0,0 +1,132 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
The true/false/None response oracle (lib/request/comparison.py).
The seqMatcher ratio path needs a live page template and is intentionally left
to --vuln. What IS pure and worth pinning here is the short-circuit decision
table: --string / --not-string / --regexp / --code matching, and the _adjust()
negative-logic flip. These are the rules that decide whether a payload counts
as True, and they are easy to break with a refactor.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.request.comparison import comparison, _adjust
from lib.core.common import removeReflectiveValues
from lib.core.settings import REFLECTED_VALUE_MARKER
from lib.core.data import conf, kb
def _reset_match_conf():
conf.string = conf.notString = conf.regexp = conf.code = None
class TestStringMatch(unittest.TestCase):
def setUp(self):
_reset_match_conf()
kb.negativeLogic = False
def tearDown(self):
_reset_match_conf()
def test_string_present_is_true(self):
conf.string = "WELCOME"
self.assertTrue(comparison("xx WELCOME yy", None, code=200))
def test_string_absent_is_false(self):
conf.string = "WELCOME"
self.assertFalse(comparison("nothing here", None, code=200))
class TestRegexpMatch(unittest.TestCase):
def setUp(self):
_reset_match_conf()
kb.negativeLogic = False
def tearDown(self):
_reset_match_conf()
def test_regexp_match_is_true(self):
conf.regexp = "id=\\d+"
self.assertTrue(comparison("user id=42 ok", None, code=200))
def test_regexp_nomatch_is_false(self):
conf.regexp = "id=\\d+"
self.assertFalse(comparison("user name", None, code=200))
class TestCodeMatch(unittest.TestCase):
def setUp(self):
_reset_match_conf()
kb.negativeLogic = False
def tearDown(self):
_reset_match_conf()
def test_code_match_is_true(self):
conf.code = 200
self.assertTrue(comparison("body", None, code=200))
def test_code_mismatch_is_false(self):
conf.code = 200
self.assertFalse(comparison("body", None, code=404))
class TestAdjustNegativeLogic(unittest.TestCase):
"""_adjust flips the condition under negative logic (the raw-page scheme),
but leaves None untouched and never flips when getRatioValue is requested."""
def setUp(self):
_reset_match_conf() # negative logic only applies with no string/regexp/code set
def tearDown(self):
_reset_match_conf()
kb.negativeLogic = False
def test_plain_passthrough(self):
kb.negativeLogic = False
self.assertEqual(_adjust(True, False), True)
self.assertEqual(_adjust(False, False), False)
def test_negative_logic_flips(self):
kb.negativeLogic = True
self.assertEqual(_adjust(True, False), False)
self.assertEqual(_adjust(False, False), True)
def test_negative_logic_leaves_none(self):
kb.negativeLogic = True
self.assertIsNone(_adjust(None, False))
class TestRemoveReflectiveValues(unittest.TestCase):
"""Reflected payloads are masked before comparison so a page echoing the
injected string isn't mistaken for a True/different response. Note: the
masking engages for *bordered* payloads (containing non-alpha chars), which
is what real injection payloads look like."""
def test_reflected_payload_is_masked(self):
out = removeReflectiveValues(u"id=1 UNION SELECT 1,2,3 end", u"1 UNION SELECT 1,2,3")
self.assertIn(REFLECTED_VALUE_MARKER, out)
self.assertNotIn(u"UNION SELECT 1,2,3", out)
def test_not_reflected_unchanged(self):
content = u"<html>nothing reflected here</html>"
self.assertEqual(removeReflectiveValues(content, u"1 AND 1=1"), content)
def test_none_payload_unchanged(self):
content = u"id=1 AND 1=1 end"
self.assertEqual(removeReflectiveValues(content, None), content)
if __name__ == "__main__":
unittest.main(verbosity=2)

141
tests/test_convert.py Normal file
View file

@ -0,0 +1,141 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Encoding / decoding / serialization round-trips and known vectors.
Covers: hex, base64 (std + url-safe), DBMS hex decode, byte<->text conversion,
JSON (de)serialization, restricted base64-pickle.
"""
import os
import random
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.convert import (decodeHex, encodeHex, decodeBase64, encodeBase64,
getBytes, getText, getUnicode, getOrds,
jsonize, dejsonize, base64pickle, base64unpickle)
from lib.core.common import decodeDbmsHexValue
from lib.core.enums import DBMS
RND = random.Random(0xC0FFEE)
def _rand_bytes(maxlen=48):
return bytes(bytearray(RND.randint(0, 255) for _ in range(RND.randint(0, maxlen))))
class TestHex(unittest.TestCase):
def test_known_vectors(self):
self.assertEqual(decodeHex("31323334", binary=True), b"1234")
self.assertEqual(getText(encodeHex(b"1234", binary=False)), "31323334")
def test_roundtrip_property(self):
for _ in range(3000):
raw = _rand_bytes()
self.assertEqual(decodeHex(encodeHex(raw, binary=False), binary=True), raw)
class TestBase64(unittest.TestCase):
def test_known_vectors(self):
self.assertEqual(decodeBase64("MTIz", binary=True), b"123")
self.assertEqual(decodeBase64("MTIzNA", binary=True), b"1234") # missing padding
self.assertEqual(decodeBase64("MTIzNA==", binary=True), b"1234")
self.assertEqual(getText(encodeBase64(b"123", binary=False)), "MTIz")
# url-safe and standard alphabets must decode equivalently
self.assertEqual(decodeBase64("A-B_CDE", binary=True), decodeBase64("A+B/CDE", binary=True))
def test_roundtrip_property(self):
for _ in range(3000):
raw = _rand_bytes()
self.assertEqual(decodeBase64(encodeBase64(raw, binary=True), binary=True), raw)
self.assertEqual(decodeBase64(encodeBase64(raw, binary=True, safe=True), binary=True), raw)
self.assertEqual(decodeBase64(encodeBase64(raw, binary=True, padding=False), binary=True), raw)
class TestDecodeDbmsHexValue(unittest.TestCase):
# authoritative vectors taken from the function's own doctests
def test_known_vectors(self):
self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1")
self.assertEqual(decodeDbmsHexValue("31003200330020003100"), u"123 1") # utf-16-le shaped
self.assertEqual(decodeDbmsHexValue("00310032003300200031"), u"123 1") # utf-16-be shaped
self.assertEqual(decodeDbmsHexValue("0x31003200330020003100"), u"123 1")
self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?") # odd length
self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"]) # list input
def test_ascii_roundtrip_property(self):
for _ in range(1000):
s = "".join(chr(RND.randint(0x20, 0x7e)) for _ in range(RND.randint(1, 30)))
if len(s) % 2 == 0: # avoid the deliberate odd-length '?' behavior
self.assertEqual(decodeDbmsHexValue(getText(encodeHex(getBytes(s), binary=False))), s)
class TestByteTextConversion(unittest.TestCase):
def test_ascii_roundtrip(self):
for _ in range(1000):
s = u"".join(unichr(RND.randint(0x20, 0x7e)) if sys.version_info[0] < 3 else chr(RND.randint(0x20, 0x7e)) for _ in range(RND.randint(0, 30)))
self.assertEqual(getUnicode(getBytes(s)), s)
def test_unicode_roundtrip(self):
samples = [u"café", u"你好", u"\U0001F600", u"ab™c"]
for s in samples:
self.assertEqual(getUnicode(getBytes(s)), s)
def test_getords(self):
self.assertEqual(getOrds(b"AB"), [65, 66])
class TestJson(unittest.TestCase):
def test_roundtrip(self):
for obj in [{"a": 1, "b": [1, 2, 3]}, [1, "x", None], {"nested": {"k": "v"}}, "str", 123]:
self.assertEqual(dejsonize(jsonize(obj)), obj)
def test_jsonize_produces_text_not_identity(self):
# anchor: jsonize must serialize to a JSON string, not pass the object through
out = jsonize({"a": 1})
self.assertIsInstance(out, str)
self.assertIn('"a"', out)
self.assertEqual(jsonize(123), "123") # int -> textual "123"
class TestBase64Pickle(unittest.TestCase):
# Types sqlmap actually serializes (injection objects, cached values, BigArray).
def test_roundtrip_allowed_types(self):
for obj in [[1, 2, 3], {"a": 1}, (1, 2), "text", 42, 3.14, True, None, {"k": [1, {"n": "v"}]}]:
self.assertEqual(base64unpickle(base64pickle(obj)), obj)
# REGRESSION: under Python 3 + PICKLE_PROTOCOL=2 a raw `bytes` value is pickled via the
# `_codecs.encode` global. The RestrictedUnpickler allowlist (patch.py) once rejected that,
# so any serialized session value containing bytes failed to load on py3. The fix allows
# exactly `_codecs.encode` (a benign codec call). Bytes MUST round-trip on both py2 and py3.
def test_bytes_roundtrip(self):
for raw in [b"x", b"\x00\x01\xff", b"\xde\xad\xbe\xef"]:
self.assertEqual(base64unpickle(base64pickle(raw)), raw, msg="bytes round-trip %r" % raw)
def test_bytes_nested_in_container_roundtrip(self):
for obj in [{"a": b"bytes"}, [b"ab", "s", 1, None], ("t", b"\xde\xad")]:
self.assertEqual(base64unpickle(base64pickle(obj)), obj, msg="nested-bytes round-trip %r" % (obj,))
def test_dangerous_globals_still_blocked(self):
# bootstrap() installs sqlmap's RestrictedUnpickler over pickle.loads. These are VALID
# pickles that reference os.system / builtins.eval - stdlib would import them happily; the
# allowlist must reject them. Assert the SPECIFIC "forbidden" ValueError (not just any
# error) so the test proves the allowlist fired, not that the bytes failed to parse.
import pickle
for payload in (b"cos\nsystem\n.", b"c__builtin__\neval\n."):
try:
pickle.loads(payload)
self.fail("dangerous global was NOT blocked: %r" % payload)
except ValueError as ex:
self.assertIn("forbidden", str(ex), msg="unexpected error for %r: %s" % (payload, ex))
if __name__ == "__main__":
unittest.main(verbosity=2)

124
tests/test_datafiles.py Normal file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Repo / data-file invariants - the cheap structural guards that catch whole
bug classes seen this session: tamper contract, per-DBMS query-tag coverage,
errors.xml regex compilation, XML well-formedness, and source ASCII-safety
(the py2 'no coding header' constraint).
"""
import os
import re
import sys
import glob
import importlib
import unittest
import xml.etree.ElementTree as ET
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, ROOT
bootstrap()
class TestTamperContract(unittest.TestCase):
def test_every_tamper_has_contract(self):
names = [os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py"))
if not f.endswith("__init__.py")]
self.assertGreater(len(names), 50) # sanity: we expect ~70
for name in names:
mod = importlib.import_module("tamper.%s" % name)
self.assertTrue(callable(getattr(mod, "tamper", None)), msg="%s: no tamper()" % name)
self.assertTrue(hasattr(mod, "__priority__"), msg="%s: no __priority__" % name)
# dependencies() is OPTIONAL (e.g. randomcomments omits it); if present it must be callable
dep = getattr(mod, "dependencies", None)
self.assertTrue(dep is None or callable(dep), msg="%s: non-callable dependencies" % name)
def test_every_tamper_priority_is_valid(self):
# __priority__ must be one of the PRIORITY enum values (or None) - a typo'd priority
# silently mis-orders the tamper chain (_setTamperingFunctions sorts on it)
from lib.core.enums import PRIORITY
valid = set(v for n, v in vars(PRIORITY).items() if not n.startswith("_"))
names = [os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py"))
if not f.endswith("__init__.py")]
for name in names:
mod = importlib.import_module("tamper.%s" % name)
priority = getattr(mod, "__priority__", None)
self.assertTrue(priority is None or priority in valid,
msg="%s: __priority__ %r is not a PRIORITY value" % (name, priority))
class TestQueriesXmlCoverage(unittest.TestCase):
CORE_TAGS = ("cast", "substring", "length", "count", "inference", "comment")
def test_every_dbms_has_core_tags(self):
tree = ET.parse(os.path.join(ROOT, "data", "xml", "queries.xml"))
dbmses = tree.findall(".//dbms")
self.assertGreaterEqual(len(dbmses), 25)
for dbms in dbmses:
present = set(child.tag for child in dbms.iter())
missing = [t for t in self.CORE_TAGS if t not in present]
self.assertEqual(missing, [], msg="%s missing core tags: %s" % (dbms.get("value"), missing))
class TestErrorsXmlCompile(unittest.TestCase):
def test_all_error_regexes_compile(self):
tree = ET.parse(os.path.join(ROOT, "data", "xml", "errors.xml"))
regexes = [e.get("regexp") for e in tree.findall(".//error")]
self.assertGreater(len(regexes), 100)
for rgx in regexes:
try:
re.compile(rgx)
except re.error as ex:
self.fail("errors.xml regex does not compile: %r (%s)" % (rgx, ex))
class TestXmlWellFormed(unittest.TestCase):
def test_core_xml_parses(self):
for rel in ("queries.xml", "boundaries.xml", "errors.xml",
os.path.join("payloads", "boolean_blind.xml"),
os.path.join("payloads", "union_query.xml")):
path = os.path.join(ROOT, "data", "xml", rel)
ET.parse(path) # raises on malformed
class TestSourceAsciiSafety(unittest.TestCase):
# sqlmap source files carry NO coding header, so any non-ASCII byte breaks py2 parsing.
# This guards the exact regression introduced (and fixed) earlier this session.
CODING_RE = re.compile(b"coding[:=]\\s*([-\\w.]+)")
def test_lib_and_plugins_are_ascii(self):
offenders = []
for base in ("lib", "plugins"):
for path in glob.glob(os.path.join(ROOT, base, "**", "*.py"), recursive=True) if sys.version_info >= (3, 5) \
else self._walk(os.path.join(ROOT, base)):
with open(path, "rb") as f:
head = f.read(256)
data = head + f.read()
if self.CODING_RE.search(head): # explicit coding header -> non-ASCII allowed
continue
try:
data.decode("ascii")
except UnicodeDecodeError:
offenders.append(os.path.relpath(path, ROOT))
self.assertEqual(offenders, [], msg="non-ASCII source w/o coding header (breaks py2): %s" % offenders)
@staticmethod
def _walk(top):
for dirpath, _, files in os.walk(top):
for fn in files:
if fn.endswith(".py"):
yield os.path.join(dirpath, fn)
class TestSettingsIntegrity(unittest.TestCase):
def test_milestone_and_version(self):
from lib.core.settings import HASHDB_MILESTONE_VALUE, VERSION
self.assertTrue(HASHDB_MILESTONE_VALUE)
self.assertTrue(re.match(r"^\d+\.\d+\.\d+", VERSION), msg="unexpected VERSION %r" % VERSION)
if __name__ == "__main__":
unittest.main(verbosity=2)

96
tests/test_datatypes.py Normal file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Core data structures: AttribDict, OrderedSet, LRUDict, BigArray.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.datatype import AttribDict, OrderedSet, LRUDict
from lib.core.bigarray import BigArray
class TestAttribDict(unittest.TestCase):
def test_attr_access(self):
a = AttribDict({"x": 1})
self.assertEqual(a.x, 1)
a.y = 2
self.assertEqual(a["y"], 2)
self.assertEqual(a.get("missing", "def"), "def")
def test_missing_attr_raises(self):
a = AttribDict()
self.assertRaises(AttributeError, lambda: a.nope)
class TestOrderedSet(unittest.TestCase):
def test_order_and_dedup(self):
s = OrderedSet()
for v in [3, 1, 3, 2, 1, 2]:
s.add(v)
self.assertEqual(list(s), [3, 1, 2])
self.assertIn(2, s)
self.assertNotIn(9, s)
self.assertEqual(len(s), 3)
class TestLRUDict(unittest.TestCase):
def test_capacity_eviction(self):
l = LRUDict(capacity=2)
l["a"] = 1
l["b"] = 2
_ = l["a"] # touch 'a' so 'b' becomes least-recently-used
l["c"] = 3 # evicts 'b'
self.assertEqual(sorted(l.keys()), ["a", "c"])
self.assertNotIn("b", l)
def test_values_retained(self):
l = LRUDict(capacity=3)
for i, k in enumerate("abc"):
l[k] = i
self.assertEqual(l["a"], 0)
self.assertEqual(l["c"], 2)
def test_capacity_one(self):
# extreme: each write evicts the previous key
l = LRUDict(capacity=1)
l["x"] = 1
l["y"] = 2
self.assertNotIn("x", l)
self.assertEqual(l["y"], 2)
self.assertEqual(list(l.keys()), ["y"])
class TestBigArray(unittest.TestCase):
def test_basic_ops(self):
b = BigArray()
for i in range(50):
b.append(i)
self.assertEqual(len(b), 50)
self.assertEqual(b[0], 0)
self.assertEqual(b[49], 49)
self.assertEqual(b[-1], 49) # negative indexing
self.assertEqual(list(b)[:3], [0, 1, 2])
def test_empty_index_raises(self):
self.assertRaises(IndexError, lambda: BigArray()[0])
def test_roundtrip_values(self):
b = BigArray()
data = list(range(100))
for v in data:
b.append(v)
self.assertEqual([b[i] for i in range(len(b))], data)
if __name__ == "__main__":
unittest.main(verbosity=2)

81
tests/test_decodepage.py Normal file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
HTTP response decoding (lib/request/basic.py decodePage).
Every fetched page passes through decodePage: it inflates gzip/deflate bodies,
applies the charset, and guards against decompression bombs. A regression here
silently corrupts every response sqlmap compares, so the round-trips and the
malformed-input handling are pinned here.
"""
import gzip
import io
import os
import sys
import unittest
import zlib
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.request.basic import decodePage
from lib.core.exception import SqlmapCompressionException
BODY = b"Hello plain body content 12345 - no markup here"
def _gzip(data):
buf = io.BytesIO()
f = gzip.GzipFile(fileobj=buf, mode="wb")
f.write(data)
f.close()
return buf.getvalue()
def _raw_deflate(data):
# decodePage uses zlib.decompressobj(-15) => raw deflate (no zlib header)
co = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS)
return co.compress(data) + co.flush()
class TestDecompression(unittest.TestCase):
def test_gzip_roundtrip(self):
# exact equality (not just substring): the whole body must decompress unchanged
out = decodePage(_gzip(BODY), "gzip", "text/html; charset=utf-8")
self.assertEqual(out, BODY.decode("utf-8"))
def test_deflate_roundtrip(self):
out = decodePage(_raw_deflate(BODY), "deflate", "text/html")
self.assertEqual(out, BODY.decode("utf-8"))
def test_identity_passthrough(self):
out = decodePage(BODY, None, "text/html")
self.assertEqual(out, BODY.decode("utf-8"))
# the exact-equality assertions above already imply a unicode return; a separate
# type-only test would be redundant.
class TestCharset(unittest.TestCase):
def test_utf8_decoded_to_unicode(self):
# several distinct multi-byte sequences (2/3/4-byte) must all decode intact
original = u"café — 你好 \U0001f512"
out = decodePage(original.encode("utf-8"), None, "text/html; charset=utf-8")
self.assertEqual(out, original)
class TestMalformed(unittest.TestCase):
def test_invalid_deflate_raises(self):
# zlib.compress() adds a 2-byte zlib header that raw-deflate decode rejects;
# body has no "<html" so decodePage surfaces it as a compression exception
self.assertRaises(SqlmapCompressionException,
lambda: decodePage(zlib.compress(BODY), "deflate", "text/plain"))
if __name__ == "__main__":
unittest.main(verbosity=2)

107
tests/test_dialect.py Normal file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Per-DBMS query building (the injection-engine "dialect" layer).
These pin the exact SQL that agent.* emits for each back-end. They are the
regression net for queries.xml edits and for dialect gates in agent.py - the
kind of change that silently mis-builds a payload for one DBMS while leaving
every other green.
Includes the SYBASE limitQuery fix: Sybase must now emit a TOP-based limited
query like MSSQL (previously it fell through and returned the query unchanged).
"""
import os
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.enums import DBMS
class TestLimitQuery(unittest.TestCase):
"""agent.limitQuery(num, query, field) per dialect (probed, not guessed)."""
def test_mysql(self):
set_dbms(DBMS.MYSQL)
self.assertEqual(agent.limitQuery(0, "SELECT name FROM users", "name"),
"SELECT name FROM users LIMIT 0,1")
def test_pgsql(self):
set_dbms(DBMS.PGSQL)
self.assertEqual(agent.limitQuery(0, "SELECT name FROM users", "name"),
"SELECT name FROM users OFFSET 0 LIMIT 1")
def test_oracle(self):
set_dbms(DBMS.ORACLE)
self.assertEqual(agent.limitQuery(0, "SELECT name FROM users", "name"),
"SELECT name FROM (SELECT name,ROWNUM AS CAP FROM users) WHERE CAP=1")
def test_mssql_is_top_based(self):
set_dbms(DBMS.MSSQL)
q = agent.limitQuery(0, "SELECT name FROM users", "name")
self.assertTrue(q.startswith("SELECT TOP 1 name FROM users WHERE"), msg=q)
self.assertIn("ORDER BY 1", q)
def test_sybase_limit_fix_is_top_based(self):
# REGRESSION: the user's limitQuery fix. Sybase must now produce a TOP-based
# limited query (mirroring MSSQL), NOT the query returned unchanged.
set_dbms(DBMS.SYBASE)
q = agent.limitQuery(0, "SELECT name FROM users", "name")
self.assertTrue(q.startswith("SELECT TOP 1 name FROM users WHERE"), msg=q)
self.assertIn("ORDER BY 1", q)
self.assertNotEqual(q, "SELECT name FROM users") # the pre-fix (broken) behavior
# Sybase casts via CONVERT(VARCHAR(...)), distinguishing it from MSSQL's NVARCHAR
self.assertIn("CONVERT(VARCHAR", q)
class TestNullAndCastField(unittest.TestCase):
"""agent.nullAndCastField('col') differs per dialect - pin each."""
CASES = {
DBMS.MYSQL: "IFNULL(CAST(col AS NCHAR),' ')",
DBMS.MSSQL: "ISNULL(CAST(col AS NVARCHAR(4000)),' ')",
DBMS.SYBASE: "ISNULL(CONVERT(VARCHAR(4000),col),' ')",
DBMS.PGSQL: "COALESCE(CAST(col AS VARCHAR(10000))::text,' ')",
DBMS.ORACLE: "NVL(CAST(col AS VARCHAR(4000)),' ')",
}
def test_per_dbms(self):
for dbms, expected in self.CASES.items():
set_dbms(dbms)
self.assertEqual(agent.nullAndCastField("col"), expected, msg="nullAndCastField for %s" % dbms)
class TestHexConvertField(unittest.TestCase):
# hexConvertField differs per dialect; pin each (was a one-platform stub before)
CASES = {
DBMS.MYSQL: "HEX(name)",
DBMS.ORACLE: "RAWTOHEX(name)",
DBMS.PGSQL: "ENCODE(CONVERT_TO((name),'UTF8'),'HEX')",
DBMS.MSSQL: "master.dbo.fn_varbintohexstr(CAST(name AS VARBINARY(8000)))",
}
def test_per_dbms(self):
for dbms, expected in self.CASES.items():
set_dbms(dbms)
self.assertEqual(agent.hexConvertField("name"), expected, msg="hexConvertField for %s" % dbms)
class TestForgeUnionQuery(unittest.TestCase):
def test_position_and_count(self):
# count=3, position=1 -> the real column is slotted at index 1, NULLs elsewhere
set_dbms(DBMS.MYSQL)
q = agent.forgeUnionQuery("SELECT a FROM t", 1, 3, None, "", "", "NULL", None)
self.assertEqual(q, " UNION ALL SELECT NULL,a,NULL FROM t")
if __name__ == "__main__":
unittest.main(verbosity=2)

88
tests/test_dicts.py Normal file
View file

@ -0,0 +1,88 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Structural invariants of the data-mapping tables in lib/core/dicts.py.
These tables drive DBMS recognition, connector selection, dummy-table dialect,
and dump formatting. They are pure data, so the right tests are shape/coverage
invariants: every back-end has a connector entry, alias lists are well-formed,
and the dialect maps carry the values the engine expects.
"""
import os
import re
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core import dicts
from lib.core.enums import DBMS
from lib.core.common import getPublicTypeMembers
class TestDbmsDict(unittest.TestCase):
def test_every_dbms_enum_has_connector_entry(self):
# DBMS_DICT keys must cover every public DBMS enum value
enum_values = set(v for _, v in getPublicTypeMembers(DBMS))
missing = enum_values - set(dicts.DBMS_DICT.keys())
self.assertEqual(missing, set(), msg="DBMS without DBMS_DICT entry: %s" % missing)
def test_entry_shape(self):
# each entry: (aliases-tuple, connector-name, connector-url, sqlalchemy-dialect)
self.assertGreaterEqual(len(dicts.DBMS_DICT), 25, msg="DBMS_DICT suspiciously small")
for name, entry in dicts.DBMS_DICT.items():
self.assertEqual(len(entry), 4, msg="malformed DBMS_DICT entry for %s" % name)
aliases = entry[0]
self.assertIsInstance(aliases, (tuple, list), msg="aliases not list-like for %s" % name)
self.assertGreaterEqual(len(aliases), 1, msg="no aliases for %s" % name)
for a in aliases: # per-item, so a failure names the offending alias
self.assertIsInstance(a, str, msg="non-str alias %r for %s" % (a, name))
def test_aliases_are_lowercase(self):
for name, entry in dicts.DBMS_DICT.items():
for alias in entry[0]:
self.assertEqual(alias, alias.lower(), msg="alias %r (for %s) is not lowercase" % (alias, name))
class TestFromDummyTable(unittest.TestCase):
def test_oracle_uses_dual(self):
self.assertEqual(dicts.FROM_DUMMY_TABLE[DBMS.ORACLE], " FROM DUAL")
def test_mysql_has_no_dummy_table(self):
# MySQL allows a bare SELECT, so it must NOT appear here
self.assertNotIn(DBMS.MYSQL, dicts.FROM_DUMMY_TABLE)
def test_values_start_with_from(self):
# strict: must be (optional leading space) FROM <whitespace> <a real table token> -
# not just startswith("FROM"), which would accept "FROMX" or a bare "FROM"
for name, clause in dicts.FROM_DUMMY_TABLE.items():
self.assertTrue(re.match(r"^\s*FROM\s+\S", clause.upper()),
msg="FROM_DUMMY_TABLE[%s]=%r is not a well-formed FROM clause" % (name, clause))
class TestSqlStatements(unittest.TestCase):
def test_known_categories_present(self):
for category in ("SQL data definition", "SQL data manipulation", "SQL data control"):
self.assertIn(category, dicts.SQL_STATEMENTS, msg="missing SQL_STATEMENTS category %r" % category)
def test_keywords_are_lowercase_tokens(self):
for category, keywords in dicts.SQL_STATEMENTS.items():
self.assertTrue(len(keywords) >= 1, msg="empty category %r" % category)
for kw in keywords:
self.assertEqual(kw, kw.lower(), msg="keyword %r in %r not lowercase" % (kw, category))
class TestDumpReplacements(unittest.TestCase):
def test_markers(self):
self.assertEqual(dicts.DUMP_REPLACEMENTS.get(""), "<blank>")
self.assertEqual(dicts.DUMP_REPLACEMENTS.get(" "), "NULL")
if __name__ == "__main__":
unittest.main(verbosity=2)

75
tests/test_encoding.py Normal file
View file

@ -0,0 +1,75 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Core text<->bytes conversions (lib/core/convert.py): getBytes, getUnicode,
getText. (getOrds is covered in test_convert.py.)
These are called on essentially every request and response, on both Python 2
and 3, and are the main thing standing between sqlmap and a UnicodeDecodeError
mid-scan. Pinned with known vectors, non-string coercion, and an encoding
round-trip property over multiple charsets.
"""
import os
import random
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.convert import getBytes, getUnicode, getText
RND = random.Random(2024)
class TestTypes(unittest.TestCase):
# value+type (not type alone): a stub returning b"" would pass an isinstance-only check, and
# on py3 a getBytes that wrongly returned str would slip past a round-trip on the unicode path
def test_getBytes_returns_bytes(self):
out = getBytes(u"abc")
self.assertIsInstance(out, bytes)
self.assertEqual(out, b"abc")
def test_getUnicode_returns_unicode(self):
out = getUnicode(b"abc")
self.assertIsInstance(out, type(u""))
self.assertEqual(out, u"abc")
def test_getText_returns_native_str(self):
self.assertIsInstance(getText(b"abc"), str)
self.assertEqual(getText(b"abc"), "abc")
class TestCoercion(unittest.TestCase):
def test_getUnicode_of_number(self):
self.assertEqual(getUnicode(123), u"123")
class TestRoundTrip(unittest.TestCase):
def test_known_utf8(self):
self.assertEqual(getUnicode(getBytes(u"caf\xe9", "utf-8"), "utf-8"), u"caf\xe9")
def test_property_multi_charset(self):
# printable BMP-ish range, round-trip through utf-8 and latin1-safe subset
for encoding, hi in (("utf-8", 0x2000), ("latin-1", 0x100)):
for _ in range(1000):
s = u"".join(unichr(RND.randint(0, hi - 1)) if sys.version_info[0] < 3
else chr(RND.randint(0, hi - 1)) for _ in range(RND.randint(0, 16)))
self.assertEqual(getUnicode(getBytes(s, encoding), encoding), s,
msg="round-trip failed (%s): %r" % (encoding, s))
# py2 has unichr, py3 does not; normalize so the file imports cleanly on both
try:
unichr
except NameError:
unichr = chr
if __name__ == "__main__":
unittest.main(verbosity=2)

113
tests/test_error_engine.py Normal file
View file

@ -0,0 +1,113 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
The error-based extraction engine (lib/techniques/error/use.py _oneShotErrorUse).
Error-based SQLi coaxes the DBMS into emitting the target value inside an error
message, wrapped between two random delimiters (kb.chars.start/stop). The engine
fires the payload and pulls the value back out with a regex. We drive the REAL
_oneShotErrorUse against a mock oracle whose "error page" embeds a known secret
between those delimiters, and assert it recovers the value exactly - no live DBMS.
Requires an error-technique injection context (kb.injection.data[...].vector with
[QUERY], plus the parameter context agent.payload needs). kb.errorChunkLength is
pre-set so the MySQL/MSSQL chunk-length probing loop is skipped.
"""
import os
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.data import conf, kb
from lib.core.datatype import AttribDict
from lib.core.enums import PAYLOAD, PLACE
from lib.request.connect import Connect
import lib.techniques.error.use as eu
def _make_vector():
d = AttribDict()
d.vector = "AND EXTRACTVALUE(1,CONCAT(0x7e,([QUERY]),0x7e))"
d.where = PAYLOAD.WHERE.ORIGINAL
d.comment = ""
d.prefix = ""
d.suffix = ""
return d
class TestOneShotErrorUse(unittest.TestCase):
def setUp(self):
self._saved = {
"conf.hexConvert": conf.get("hexConvert"), "conf.charset": conf.get("charset"),
"conf.hashDB": conf.get("hashDB"), "conf.parameters": conf.get("parameters"),
"conf.paramDict": conf.get("paramDict"), "conf.base64Parameter": conf.get("base64Parameter"),
"kb.errorChunkLength": kb.get("errorChunkLength"), "kb.testMode": kb.get("testMode"),
"kb.forceWhere": kb.get("forceWhere"), "kb.technique": kb.get("technique"),
"kb.inj": (kb.injection.place, kb.injection.parameter, kb.injection.data),
"qp": Connect.queryPage,
}
conf.hexConvert = False
conf.charset = None
conf.hashDB = None
conf.parameters = {PLACE.GET: "id=1"}
conf.paramDict = {PLACE.GET: {"id": "1"}}
conf.base64Parameter = ()
kb.errorChunkLength = 0
kb.testMode = False
kb.forceWhere = None
kb.injection.place = PLACE.GET
kb.injection.parameter = "id"
kb.technique = PAYLOAD.TECHNIQUE.ERROR
kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: _make_vector()}
set_dbms("MySQL")
def tearDown(self):
conf.hexConvert = self._saved["conf.hexConvert"]
conf.charset = self._saved["conf.charset"]
conf.hashDB = self._saved["conf.hashDB"]
conf.parameters = self._saved["conf.parameters"]
conf.paramDict = self._saved["conf.paramDict"]
conf.base64Parameter = self._saved["conf.base64Parameter"]
kb.errorChunkLength = self._saved["kb.errorChunkLength"]
kb.testMode = self._saved["kb.testMode"]
kb.forceWhere = self._saved["kb.forceWhere"]
kb.technique = self._saved["kb.technique"]
kb.injection.place, kb.injection.parameter, kb.injection.data = self._saved["kb.inj"]
Connect.queryPage = self._saved["qp"]
eu.Request.queryPage = self._saved["qp"]
def _extract(self, secret, page_template="XPATH syntax error: '%s%s%s'"):
def oracle(payload=None, content=False, raise404=True, **kwargs):
page = page_template % (kb.chars.start, secret, kb.chars.stop)
return (page, {}, 200) if content else True
Connect.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
return eu._oneShotErrorUse("SELECT CONCAT(user())")
def test_simple_value(self):
self.assertEqual(self._extract("root@localhost"), "root@localhost")
def test_version_string(self):
self.assertEqual(self._extract("5.7.31-0ubuntu0.18.04.1-log"), "5.7.31-0ubuntu0.18.04.1-log")
def test_value_with_symbols(self):
self.assertEqual(self._extract("a-b_c.d:e/f"), "a-b_c.d:e/f")
def test_no_markers_returns_none(self):
def oracle(payload=None, content=False, raise404=True, **kwargs):
return ("a perfectly ordinary page with no error", {}, 200) if content else True
Connect.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
self.assertIsNone(eu._oneShotErrorUse("SELECT CONCAT(user())"))
if __name__ == "__main__":
unittest.main(verbosity=2)

105
tests/test_hash.py Normal file
View file

@ -0,0 +1,105 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Password-hashing primitives (lib/utils/hash.py) used by the dictionary-attack
cracker (-? / --passwords). These are pure functions; correctness here is what
makes a cracked password actually match the target hash.
The generic hashes are cross-checked against the stdlib hashlib (an INDEPENDENT
oracle, not just a regression against sqlmap's own output). The DBMS-specific
algorithms (MySQL/MSSQL/Oracle/Postgres) are pinned to known vectors, and
hashRecognition's classification is exercised as a table.
"""
import hashlib
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.utils import hash as H
from lib.core.enums import HASH
class TestGenericVsHashlib(unittest.TestCase):
"""Independent oracle: sqlmap's generic hashes must equal stdlib hashlib."""
PW = "testpass"
def test_md5(self):
self.assertEqual(H.md5_generic_passwd(self.PW), hashlib.md5(b"testpass").hexdigest())
def test_sha1(self):
self.assertEqual(H.sha1_generic_passwd(self.PW), hashlib.sha1(b"testpass").hexdigest())
def test_sha224(self):
self.assertEqual(H.sha224_generic_passwd(self.PW), hashlib.sha224(b"testpass").hexdigest())
def test_sha256(self):
self.assertEqual(H.sha256_generic_passwd(self.PW), hashlib.sha256(b"testpass").hexdigest())
def test_sha384(self):
self.assertEqual(H.sha384_generic_passwd(self.PW), hashlib.sha384(b"testpass").hexdigest())
def test_sha512(self):
self.assertEqual(H.sha512_generic_passwd(self.PW), hashlib.sha512(b"testpass").hexdigest())
class TestUppercase(unittest.TestCase):
def test_uppercase_flag(self):
self.assertEqual(H.md5_generic_passwd("testpass", uppercase=True),
hashlib.md5(b"testpass").hexdigest().upper())
def test_lowercase_default(self):
out = H.md5_generic_passwd("testpass", uppercase=False)
self.assertEqual(out, out.lower())
class TestDbmsSpecificVectors(unittest.TestCase):
"""Known vectors for the DBMS-native algorithms (mirrors the docstrings)."""
def test_mysql(self):
self.assertEqual(H.mysql_passwd("testpass", uppercase=True),
"*00E247AC5F9AF26AE0194B41E1E769DEE1429A29")
def test_mysql_old(self):
self.assertEqual(H.mysql_old_passwd("testpass", uppercase=True), "7DCDA0D57290B453")
def test_postgres(self):
self.assertEqual(H.postgres_passwd("testpass", "testuser", uppercase=False),
"md599e5ea7a6f7c3269995cba3927fd0093")
def test_mssql(self):
self.assertEqual(H.mssql_passwd("testpass", salt="4086ceb6", uppercase=False),
"0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a")
def test_oracle(self):
self.assertEqual(H.oracle_passwd("SHAlala", salt="1B7B5F82B7235E9E182C", uppercase=True),
"S:2BFCFDF5895014EE9BB2B9BA067B01E0389BB5711B7B5F82B7235E9E182C")
def test_oracle_old(self):
self.assertEqual(H.oracle_old_passwd("tiger", "scott", uppercase=True), "F894844C34402B67")
class TestHashRecognition(unittest.TestCase):
def test_md5_generic(self):
self.assertEqual(H.hashRecognition("179ad45c6ce2cb97cf1029e212046e81"), HASH.MD5_GENERIC)
def test_sha1_generic(self):
self.assertEqual(H.hashRecognition("206c80413b9a96c1312cc346b7d2517b84463edd"), HASH.SHA1_GENERIC)
def test_mysql(self):
self.assertEqual(H.hashRecognition("*00E247AC5F9AF26AE0194B41E1E769DEE1429A29"), HASH.MYSQL)
def test_junk_is_none(self):
self.assertIsNone(H.hashRecognition("foobar"))
if __name__ == "__main__":
unittest.main(verbosity=2)

129
tests/test_hashdb.py Normal file
View file

@ -0,0 +1,129 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Session storage layer (lib/utils/hashdb.py) - the on-disk SQLite cache that
makes --flush-session / resume work.
Exercised against a REAL temporary SQLite file (no network, no DBMS): scalar
write/retrieve, serialized round-trip for every container type sqlmap stores,
overwrite semantics, missing-key -> None, and key-hash determinism.
This is also the end-to-end regression for the base64-pickle bytes fix: a
serialized value containing raw `bytes` must survive a write/flush/retrieve
cycle on both Python 2 and 3 (it silently failed on py3 before the patch.py fix).
"""
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.utils.hashdb import HashDB
from lib.core.datatype import AttribDict
from lib.core.bigarray import BigArray
class _HashDBCase(unittest.TestCase):
def setUp(self):
fd, self.path = tempfile.mkstemp(suffix=".sqlite")
os.close(fd)
os.remove(self.path) # HashDB creates it lazily
self.db = HashDB(self.path)
def tearDown(self):
try:
self.db.closeAll()
except Exception:
pass
if os.path.exists(self.path):
os.remove(self.path)
class TestScalar(_HashDBCase):
def test_string_roundtrip(self):
self.db.write("greeting", "hello")
self.db.flush()
self.assertEqual(self.db.retrieve("greeting"), "hello")
def test_non_serialized_number_comes_back_as_text(self):
# non-serialized writes are stored via getUnicode()
self.db.write("num", 5)
self.db.flush()
self.assertEqual(self.db.retrieve("num"), "5")
def test_missing_key_is_none(self):
self.assertIsNone(self.db.retrieve("never-written"))
def test_overwrite_last_wins(self):
self.db.write("k", "v1")
self.db.write("k", "v2")
self.db.flush()
self.assertEqual(self.db.retrieve("k"), "v2")
def test_keys_are_independent(self):
self.db.write("a", "1")
self.db.write("b", "2")
self.db.flush()
self.assertEqual(self.db.retrieve("a"), "1")
self.assertEqual(self.db.retrieve("b"), "2")
class TestSerialized(_HashDBCase):
def test_list_dict_tuple_set(self):
cases = {
"list": [1, 2, 3, "x"],
"dict": {"k": [1, {"n": "v"}]},
"tuple": (1, "a", None),
"set": set([1, 2, 3]),
}
for key, val in cases.items():
self.db.write(key, val, True)
self.db.flush()
for key, val in cases.items():
self.assertEqual(self.db.retrieve(key, True), val, msg="serialized round-trip for %s" % key)
def test_attribdict_roundtrip(self):
ad = AttribDict()
ad.x = 1
ad.y = [1, 2]
self.db.write("ad", ad, True)
self.db.flush()
got = self.db.retrieve("ad", True)
self.assertIsInstance(got, AttribDict)
self.assertEqual(got.x, 1)
self.assertEqual(got.y, [1, 2])
def test_bigarray_roundtrip(self):
self.db.write("ba", BigArray([1, 2, 3]), True)
self.db.flush()
got = self.db.retrieve("ba", True)
self.assertIsInstance(got, BigArray)
self.assertEqual(list(got), [1, 2, 3])
def test_bytes_containing_value_survives(self):
# REGRESSION (base64-pickle bytes fix): silently failed to restore on py3 before the fix.
value = {"raw": b"\x00\x01\xff", "items": [b"ab", "s", 1]}
self.db.write("bytesval", value, True)
self.db.flush()
self.assertEqual(self.db.retrieve("bytesval", True), value)
class TestKeyHashing(_HashDBCase):
def test_distinct_keys_distinct_hashes(self):
# a broken hashKey that keys only on (say) length or the last char would collide; require
# 200 distinct keys to map to 200 distinct hashes. (Determinism is implied: the retrieve
# round-trips in TestScalar already depend on hashKey being stable.)
keys = ["key_%d_%s" % (i, "abcdefgh"[i % 8]) for i in range(200)]
hashes = set(HashDB.hashKey(k) for k in keys)
self.assertEqual(len(hashes), len(keys), msg="hashKey produced collisions across distinct keys")
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -0,0 +1,78 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Identifier quoting per DBMS dialect, CSV value escaping, and dump value
replacement markers.
"""
import os
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.common import safeSQLIdentificatorNaming, unsafeSQLIdentificatorNaming, safeCSValue
from lib.core.enums import DBMS
class TestIdentifierQuoting(unittest.TestCase):
# special-char identifier -> the per-dialect quoting wrapper
WRAP = {
DBMS.MYSQL: "`weird name`",
DBMS.MSSQL: "[weird name]",
DBMS.PGSQL: '"weird name"',
DBMS.ORACLE: '"WEIRD NAME"', # Oracle upper-cases quoted identifiers
}
def test_special_identifier_quoting(self):
for dbms, wrapped in self.WRAP.items():
set_dbms(dbms)
self.assertEqual(safeSQLIdentificatorNaming("weird name"), wrapped, msg=str(dbms))
def test_simple_identifier_roundtrip(self):
# plain identifier needs no quoting; round-trips identically on case-preserving dialects
for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL):
set_dbms(dbms)
for ident in ("users", "password", "tbl1"):
self.assertEqual(safeSQLIdentificatorNaming(ident), ident, msg="%s %r" % (dbms, ident))
self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming(ident)), ident)
def test_oracle_uppercases_on_unsafe(self):
# documented dialect quirk: Oracle unsafe-naming upper-cases identifiers
set_dbms(DBMS.ORACLE)
self.assertEqual(safeSQLIdentificatorNaming("users"), "users")
self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming("users")), "USERS")
def test_unsafe_strips_quotes(self):
for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL):
set_dbms(dbms)
self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming("weird name")), "weird name")
class TestSafeCSValue(unittest.TestCase):
CASES = [
("foobar", "foobar"), # plain -> unchanged
("foo,bar", '"foo,bar"'), # contains delimiter -> quoted
('he"y', '"he""y"'), # contains quote -> doubled + wrapped
("a\nb", '"a\nb"'), # contains newline -> quoted
]
def test_table(self):
for inp, expected in self.CASES:
self.assertEqual(safeCSValue(inp), expected, msg="safeCSValue(%r)" % inp)
def test_idempotent_on_already_quoted(self):
once = safeCSValue("a,b")
self.assertEqual(safeCSValue(once), once) # already starts+ends with quote -> unchanged
# (DUMP_REPLACEMENTS markers are covered in test_dicts.py - not duplicated here)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -0,0 +1,153 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
The blind-SQLi extraction engine (lib/techniques/blind/inference.py bisection).
This is the actual algorithm that pulls data out one character at a time over a
boolean/blind oracle - the heart of sqlmap. It is normally network-coupled, so
here we drive the REAL bisection() against a mock oracle: Request.queryPage is
replaced with a function that decodes the forged payload (we control the payload
template, so it is trivially parseable) and answers the comparison against a
known secret. If bisection's binary search, charset narrowing, or value assembly
regress, these go red - without a live target.
Also asserts the search is logarithmic (binary search), not a linear scan of the
character space.
"""
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.data import conf, kb
from lib.core.common import getCurrentThreadData
from lib.request.connect import Connect
import lib.techniques.blind.inference as inf
# bisection does: safeStringFormat(payload, (expression, idx, posValue)); '>' is the
# greater-char marker (swapped to '=' on the final equality check). We pass a parseable
# template so the mock oracle can recover (idx, operator, threshold).
TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)")
# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path
_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False,
"charset": None, "firstChar": None, "lastChar": None, "timeSec": 5}
_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False,
"disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False}
class _EngineCase(unittest.TestCase):
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in _CONF}
self._saved_kb = {k: kb.get(k) for k in _KB}
self._saved_qp = Connect.queryPage
self._saved_processChar = kb.data.get("processChar")
for k, v in _CONF.items():
conf[k] = v
for k, v in _KB.items():
kb[k] = v
kb.data.processChar = None
set_dbms("MySQL")
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
for k, v in self._saved_kb.items():
kb[k] = v
kb.data.processChar = self._saved_processChar
Connect.queryPage = self._saved_qp
inf.Request.queryPage = self._saved_qp
def _extract(self, secret, charsetType=None):
def oracle(payload=None, *args, **kwargs):
m = _PARSE.search(payload)
idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3))
ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0
return (ch > threshold) if op == ">" else (ch == threshold)
Connect.queryPage = staticmethod(oracle)
inf.Request.queryPage = staticmethod(oracle)
td = getCurrentThreadData()
td.shared.value = ""
td.shared.index = [0]
td.shared.start = 0
td.shared.count = 0
count, value = inf.bisection(TEMPLATE, "SELECT secret", length=len(secret), charsetType=charsetType)
return value, count
class TestBisectionExtraction(_EngineCase):
# NOTE: the alpha / numeric / mixed cases are NOT redundant - getChar has per-class
# "first character" position heuristics (distinct branches for a-z, A-Z and 0-9 at
# inference.py ~331-336), so each character class exercises a different code path.
def test_single_char(self):
value, _ = self._extract("X")
self.assertEqual(value, "X")
def test_alpha(self):
value, _ = self._extract("AdminUser") # exercises the a-z / A-Z heuristic branch
self.assertEqual(value, "AdminUser")
def test_alphanumeric(self):
value, _ = self._extract("admin123")
self.assertEqual(value, "admin123")
def test_with_spaces_and_symbols(self):
value, _ = self._extract("p@ss W0rd!")
self.assertEqual(value, "p@ss W0rd!")
def test_numeric_string(self):
value, _ = self._extract("4815162342") # exercises the 0-9 heuristic branch
self.assertEqual(value, "4815162342")
def test_longer_value(self):
secret = "The quick brown fox 0123456789"
value, _ = self._extract(secret)
self.assertEqual(value, secret)
class TestUnicodeExpansion(_EngineCase):
"""charsetType=None starts with a 0..127 table and gradually expands it (shiftTable) to
reach higher code points. This test exercises the FIRST expansion step (code points
128..1023) via Latin-1 chars, where the per-byte oracle model is exact.
NOTE: kb.disableShiftTable is an INTENTIONAL session-level safety latch (sqlmap author's
design): once expansion runs all the way to the top - only reachable by a code point above
0xFFFFF, or by a misbehaving always-TRUE oracle - it disables further expansion to prevent
runaway / erroneous extraction. That is deliberate, so this test does NOT assert that
expansion survives across such an event.
(Code points >= 256 are retrieved/assembled byte-wise in real runs - decodeIntToUnicode
splits them into a byte sequence - so a simple ord()-based mock oracle only models the
single-byte range; those are out of scope here.)"""
def test_extracts_latin1_via_first_expansion(self):
for s in (u"caf\xe9", u"\xfcber", u"ni\xf1o", u"\xe9\xe8\xea\xeb"):
self.assertEqual(self._extract(s)[0], s, msg="expansion extraction failed for %r" % s)
class TestSearchIsLogarithmic(_EngineCase):
def test_query_count_is_sublinear_in_charset(self):
# GOAL: catch a regression from binary search to a linear/per-codepoint scan.
# Observed cost is ~6-22 queries/char (it varies: the first-char heuristic's benefit
# depends on ambient kb/conf state, so a tighter bound would flake). A linear scan of the
# 128-char ASCII space would be ~128/char (~3840 for 30 chars). Bound at 40/char cleanly
# separates "logarithmic" (passes) from "linearized" (fails) without being flaky.
secret = "x" * 30
_, count = self._extract(secret)
self.assertLess(count, len(secret) * 40,
msg="bisection used %d queries for %d chars (~%.1f/char) - search regressed toward linear?"
% (count, len(secret), count / float(len(secret))))
if __name__ == "__main__":
unittest.main(verbosity=2)

125
tests/test_misc.py Normal file
View file

@ -0,0 +1,125 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Assorted pure helpers: stats, set ops, value predicates, value/counter stacks,
enum helpers, DBMS alias/version checks, column prioritization.
"""
import os
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 import common as C
from lib.core.settings import NULL
from lib.core.enums import DBMS
class TestStats(unittest.TestCase):
def test_average(self):
self.assertEqual(C.average([1, 2, 3, 4]), 2.5)
self.assertEqual(C.average([5]), 5)
def test_stdev(self):
self.assertAlmostEqual(C.stdev([1, 2, 3, 4]), 1.2909944, places=5)
self.assertIsNone(C.stdev([5])) # undefined for single sample
class TestSetOps(unittest.TestCase):
def test_intersect(self):
self.assertEqual(C.intersect([1, 2, 3], [2, 3, 4]), [2, 3])
self.assertEqual(C.intersect([1], [2]), [])
def test_filterPairValues(self):
self.assertEqual(C.filterPairValues([[1, 2], [3], [4, 5], []]), [[1, 2], [4, 5]])
class TestValuePredicates(unittest.TestCase):
def test_isNoneValue(self):
for v in (None, [], "", {}):
self.assertTrue(C.isNoneValue(v), msg="isNoneValue(%r)" % (v,))
def test_isNullValue(self):
self.assertTrue(C.isNullValue(NULL))
# discriminating negatives: an always-True impl must fail these
self.assertFalse(C.isNullValue(None))
self.assertFalse(C.isNullValue(""))
self.assertFalse(C.isNullValue("x"))
def test_isNumPosStrValue(self):
for v, exp in [("5", True), ("0", False), ("-1", False), ("a", False), ("12", True)]:
self.assertEqual(bool(C.isNumPosStrValue(v)), exp, msg="isNumPosStrValue(%r)" % v)
def test_firstNotNone(self):
self.assertEqual(C.firstNotNone(None, None, 5, 6), 5)
self.assertIsNone(C.firstNotNone(None, None))
class TestValueStackAndCounters(unittest.TestCase):
def test_push_pop(self):
C.pushValue(7)
C.pushValue("x")
self.assertEqual(C.popValue(), "x")
self.assertEqual(C.popValue(), 7)
def test_counters(self):
C.resetCounter("UNITTEST")
C.incrementCounter("UNITTEST")
C.incrementCounter("UNITTEST")
self.assertEqual(C.getCounter("UNITTEST"), 2)
class TestEnumAndDbmsHelpers(unittest.TestCase):
def test_aliasToDbmsEnum(self):
self.assertEqual(C.aliasToDbmsEnum("mysql"), DBMS.MYSQL)
self.assertEqual(C.aliasToDbmsEnum("postgres"), DBMS.PGSQL)
def test_getPublicTypeMembers(self):
members = list(C.getPublicTypeMembers(DBMS, onlyValues=True))
# goal is correct EXTRACTION, not a magic count: real members present, no private/dunder leak
self.assertIn(DBMS.MYSQL, members)
self.assertIn(DBMS.MSSQL, members)
self.assertIn(DBMS.ORACLE, members)
self.assertFalse(any(str(m).startswith("_") for m in members), msg="leaked private member: %r" % members)
def test_isDBMSVersionAtLeast(self):
set_dbms(DBMS.MYSQL)
C.Backend.setVersion("5.7")
self.assertTrue(C.isDBMSVersionAtLeast("5.0"))
self.assertFalse(C.isDBMSVersionAtLeast("8.0"))
class TestColumnPriority(unittest.TestCase):
def test_prioritySortColumns(self):
# assert the FULL ordering, not just the first element (id-like floats to front,
# rest keep their relative order)
self.assertEqual(C.prioritySortColumns(["data", "id", "name"]), ["id", "data", "name"])
def test_prioritySortColumns_empty(self):
self.assertEqual(C.prioritySortColumns([]), [])
class TestArrayHelpers(unittest.TestCase):
def test_unArrayizeValue(self):
self.assertEqual(C.unArrayizeValue([5]), 5) # single-element list -> the element
self.assertEqual(C.unArrayizeValue([1, 2]), 1) # multi -> first
self.assertEqual(C.unArrayizeValue(7), 7) # scalar -> unchanged
self.assertIsNone(C.unArrayizeValue([])) # empty -> None
def test_arrayizeValue(self):
self.assertEqual(C.arrayizeValue(5), [5]) # scalar -> wrapped
self.assertEqual(C.arrayizeValue([5]), [5]) # list -> unchanged
def test_roundtrip_scalar(self):
for v in (0, 1, "x", "value"):
self.assertEqual(C.unArrayizeValue(C.arrayizeValue(v)), v)
if __name__ == "__main__":
unittest.main(verbosity=2)

85
tests/test_pagecontent.py Normal file
View file

@ -0,0 +1,85 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Page-content extraction helpers (lib/core/common.py): getFilteredPageContent,
getPageWordSet, extractTextTagContent, and parseSqliteTableSchema.
The first three feed text-only comparison (--text-only), dynamic-content
removal, and Google-dork style scraping; the last reconstructs column metadata
from a sqlite_master CREATE TABLE statement during enumeration. All pure given
their input (the page must be unicode for tag stripping to engage - a real
gotcha pinned below).
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import (getFilteredPageContent, getPageWordSet,
extractTextTagContent, parseSqliteTableSchema)
from lib.core.data import kb
class TestFilteredPageContent(unittest.TestCase):
def test_strips_all_tags_in_text_mode(self):
self.assertEqual(getFilteredPageContent(u"<html><title>foobar</title><body>test</body></html>"),
u"foobar test")
def test_strips_script(self):
self.assertEqual(getFilteredPageContent(u"<p>keep</p><script>var x=1;</script><p>this</p>"),
u"keep this")
def test_keeps_tags_when_not_only_text(self):
self.assertEqual(getFilteredPageContent(u"<p>a</p><script>x</script><p>b</p>", onlyText=False),
u"<p>a</p> <p>b</p>")
def test_bytes_input_unchanged(self):
# GOTCHA: tag stripping only engages for unicode input (charset-identified pages)
raw = b"<b>x</b>"
self.assertEqual(getFilteredPageContent(raw), raw)
class TestPageWordSet(unittest.TestCase):
def test_words(self):
self.assertEqual(sorted(getPageWordSet(u"<html><title>foobar</title><body>test</body></html>")),
[u"foobar", u"test"])
class TestExtractTextTagContent(unittest.TestCase):
def test_multiple_tags(self):
self.assertEqual(extractTextTagContent(u"<title>Welcome</title><p>Body text</p>"),
[u"Welcome", u"Body text"])
class TestParseSqliteTableSchema(unittest.TestCase):
def setUp(self):
kb.data.cachedColumns = {}
def _cols(self):
# parseSqliteTableSchema stores under cachedColumns[db][table] (both None here)
return dict(kb.data.cachedColumns[None][None])
def test_basic_columns_and_types(self):
parseSqliteTableSchema("CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INT)")
cols = self._cols()
self.assertEqual(cols["id"], "INTEGER")
self.assertEqual(cols["name"], "TEXT")
self.assertEqual(cols["age"], "INT")
def test_quoted_identifiers_and_sized_types(self):
parseSqliteTableSchema('CREATE TABLE "t"("id" INTEGER, "n" VARCHAR(50), flag BOOLEAN)')
cols = self._cols()
self.assertIn("id", cols)
self.assertEqual(cols["n"], "VARCHAR") # size dropped
self.assertEqual(cols["flag"], "BOOLEAN")
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -0,0 +1,157 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Request-body injection-point handling:
- recognition regexes (REAL, imported from settings) classify JSON/JSON_LIKE/XML/PLAIN
- JSON/XML injection-point marking preserves every value (mirrors target.py)
- HPP transform reconstructs the original SQL after ASP comma-join
"""
import os
import re
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.settings import (JSON_RECOGNITION_REGEX, JSON_LIKE_RECOGNITION_REGEX,
XML_RECOGNITION_REGEX, PAYLOAD_DELIMITER, DEFAULT_GET_POST_DELIMITER)
MARK = "*"
def classify(d):
if re.search(JSON_RECOGNITION_REGEX, d):
return "JSON"
if re.search(JSON_LIKE_RECOGNITION_REGEX, d):
return "JSON_LIKE"
if re.search(XML_RECOGNITION_REGEX, d):
return "XML"
return "PLAIN"
class TestRecognitionRegexes(unittest.TestCase):
CASES = [
('{"id":1}', "JSON"),
('{"a":"b"}', "JSON"),
('{"n":1,"m":"s"}', "JSON"),
('[{"id":1}]', "JSON"),
('[{"id":1},{"id":2}]', "JSON"),
("{'a':'b'}", "JSON_LIKE"),
("<a>1</a>", "XML"),
("<soap:Body><q>1</q></soap:Body>", "XML"),
("<ns:tag attr='x'>v</ns:tag>", "XML"),
("id=1&x=2", "PLAIN"),
("just text", "PLAIN"),
]
def test_classification(self):
for body, expected in self.CASES:
self.assertEqual(classify(body), expected, msg="classify(%r)" % body)
class TestJsonMarking(unittest.TestCase):
# mirrors target.py:159-162 JSON injection-point marking
@staticmethod
def mark(data):
data = re.sub(r'("(?P<name>[^"]+)"\s*:\s*".*?)"(?<!\\")', r'\g<1>%s"' % MARK, data)
data = re.sub(r'("(?P<name>[^"]+)"\s*:\s*")"', r'\g<1>%s"' % MARK, data)
data = re.sub(r'("(?P<name>[^"]+)"\s*:\s*)(-?\d[\d\.]*)\b', r'\g<1>\g<3>%s' % MARK, data)
data = re.sub(r'("(?P<name>[^"]+)"\s*:\s*)((true|false|null))\b', r'\g<1>\g<3>%s' % MARK, data)
return data
CASES = [
('{"id":1}', '{"id":1*}'),
('{"name":"abc"}', '{"name":"abc*"}'),
('{"a":{"b":"1"}}', '{"a":{"b":"1*"}}'),
('{"empty":""}', '{"empty":"*"}'),
('{"b":true,"n":null}', '{"b":true*,"n":null*}'),
('{"a":"x","b":"y"}', '{"a":"x*","b":"y*"}'),
('{"url":"http://h:8080/p"}', '{"url":"http://h:8080/p*"}'),
]
def test_cases(self):
for inp, expected in self.CASES:
self.assertEqual(self.mark(inp), expected, msg="mark(%r)" % inp)
def test_value_preserved_property(self):
# marking must not delete/garble the original value characters
for inp, _ in self.CASES:
out = self.mark(inp)
self.assertEqual(out.replace(MARK, ""), inp, msg="marking altered %r" % inp)
class TestXmlMarking(unittest.TestCase):
RX = r"(<(?P<name>[^>]+)( [^<]*)?>)([^<]+)(</\2)"
def mark(self, data):
return re.sub(self.RX, r"\g<1>\g<4>%s\g<5>" % MARK, data)
CASES = [
("<a>x</a>", "<a>x*</a>"),
('<a id="1">x</a>', '<a id="1">x*</a>'),
("<user><name>bob</name><id>5</id></user>", "<user><name>bob*</name><id>5*</id></user>"),
("<ns:tag>v</ns:tag>", "<ns:tag>v*</ns:tag>"),
("<soap:Body><q>1</q></soap:Body>", "<soap:Body><q>1*</q></soap:Body>"),
]
def test_cases(self):
for inp, expected in self.CASES:
self.assertEqual(self.mark(inp), expected, msg="xmlmark(%r)" % inp)
class TestHppReconstruction(unittest.TestCase):
# mirrors connect.py:1171-1187 HPP splitting
def hpp(self, payload, name="id"):
from thirdparty.six.moves import urllib as _urllib # py2+py3
quote = _urllib.parse.quote
def ue(s):
try:
return quote(s)
except Exception:
return s
value = "%s=%s%s%s" % (name, PAYLOAD_DELIMITER, payload, PAYLOAD_DELIMITER)
_ = re.escape(PAYLOAD_DELIMITER)
match = re.search(r"(?P<name>\w+)=%s(?P<value>.+?)%s" % (_, _), value)
out = match.group("value")
for splitter in (ue(' '), ' '):
if splitter in out:
prefix, suffix = ("*/", "/*") if splitter == ' ' else (ue(x) for x in ("*/", "/*"))
parts = out.split(splitter)
parts[0] = "%s%s" % (parts[0], suffix)
parts[-1] = "%s%s=%s%s" % (DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[-1])
for i in range(1, len(parts) - 1):
parts[i] = "%s%s=%s%s%s" % (DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[i], suffix)
out = "".join(parts)
for splitter in (ue(','), ','):
out = out.replace(splitter, "%s%s=" % (DEFAULT_GET_POST_DELIMITER, match.group("name")))
return out
# Exact transform outputs (verified live against an ASP-style join). We pin the produced
# string rather than "reconstruct the SQL", because reconstruction depends on the SQL parser
# treating /* */ as a token separator (1/*,*/AND -> "1 AND"), which a string compare can't model.
CASES = [
("1", "1"),
("1 AND 2=2", "1/*&id=*/AND/*&id=*/2=2"),
("1 AND 'a'='a'", "1/*&id=*/AND/*&id=*/'a'='a'"),
]
def test_exact_outputs(self):
for payload, expected in self.CASES:
self.assertEqual(self.hpp(payload), expected, msg="hpp(%r)" % payload)
def test_balanced_comments(self):
# every /* must have a matching */ (no dangling comment bridge)
for payload in ["1 UNION SELECT a,b", "1 AND 2=2 OR 3=3", "x y z"]:
out = self.hpp(payload)
self.assertEqual(out.count("/*"), out.count("*/"), msg="unbalanced comments for %r" % payload)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -0,0 +1,110 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Structural invariants of the injection payload/boundary definitions
(data/xml/payloads/*.xml -> conf.tests, data/xml/boundaries.xml -> conf.boundaries).
These XML files ARE the detection engine: every test/boundary loaded here is
something sqlmap will fire at a target. The fields are pure data, so the right
tests are shape/range invariants - a malformed level, an unknown technique, a
duplicate title, or a test missing its request payload would silently break or
skew detection.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.parse.payloads import loadBoundaries, loadPayloads
from lib.core.data import conf
from lib.core.enums import PAYLOAD
from lib.core.common import getPublicTypeMembers
# load once for the module
loadBoundaries()
loadPayloads()
TECHNIQUES = set(v for _, v in getPublicTypeMembers(PAYLOAD.TECHNIQUE)) # {1..6}
WHERES = set(v for _, v in getPublicTypeMembers(PAYLOAD.WHERE)) # {1,2,3}
class TestLoaded(unittest.TestCase):
# floors well below the current counts (~340 tests, ~54 boundaries) - high enough to catch a
# truncated/partially-loaded XML set (not just "> 0"), low enough to survive normal additions
def test_payloads_loaded(self):
self.assertGreaterEqual(len(conf.tests), 200, msg="only %d tests loaded" % len(conf.tests))
def test_boundaries_loaded(self):
self.assertGreaterEqual(len(conf.boundaries), 30, msg="only %d boundaries loaded" % len(conf.boundaries))
class TestTestEntries(unittest.TestCase):
def setUp(self):
# guard against vacuous passes: if payloads failed to load, every loop below
# would iterate zero times and pass silently
self.assertTrue(conf.tests, "conf.tests is empty - payloads failed to load")
def test_required_fields_present(self):
for t in conf.tests:
for field in ("title", "stype", "clause", "where", "level", "risk", "request", "response"):
self.assertIn(field, t, msg="test %r missing field %r" % (t.get("title"), field))
def test_title_non_empty(self):
for t in conf.tests:
self.assertTrue(t.title and t.title.strip(), msg="empty test title")
def test_titles_unique(self):
titles = [t.title for t in conf.tests]
self.assertEqual(len(titles), len(set(titles)), msg="duplicate test titles exist")
def test_stype_is_known_technique(self):
for t in conf.tests:
self.assertIn(t.stype, TECHNIQUES, msg="test %r has unknown stype %r" % (t.title, t.stype))
def test_level_and_risk_in_range(self):
for t in conf.tests:
self.assertIn(t.level, (1, 2, 3, 4, 5), msg="test %r bad level %r" % (t.title, t.level))
self.assertIn(t.risk, (1, 2, 3), msg="test %r bad risk %r" % (t.title, t.risk))
def test_request_has_payload(self):
for t in conf.tests:
self.assertIn("payload", t.request, msg="test %r request has no payload" % t.title)
def test_where_values_valid(self):
for t in conf.tests:
for w in t.where:
self.assertIn(w, WHERES, msg="test %r has bad where %r" % (t.title, w))
class TestBoundaryEntries(unittest.TestCase):
def setUp(self):
self.assertTrue(conf.boundaries, "conf.boundaries is empty - boundaries failed to load")
def test_required_fields_present(self):
for b in conf.boundaries:
for field in ("level", "clause", "where", "ptype"):
self.assertIn(field, b, msg="boundary missing field %r" % field)
def test_level_in_range(self):
for b in conf.boundaries:
self.assertIn(b.level, (1, 2, 3, 4, 5), msg="boundary bad level %r" % b.level)
def test_where_values_valid(self):
for b in conf.boundaries:
for w in b.where:
self.assertIn(w, WHERES, msg="boundary bad where %r" % w)
def test_clause_is_list_like(self):
for b in conf.boundaries:
self.assertTrue(isinstance(b.clause, (list, tuple)), msg="boundary clause not list-like")
if __name__ == "__main__":
unittest.main(verbosity=2)

87
tests/test_replication.py Normal file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
SQLite replication writer (lib/core/replication.py).
This is what backs `--dump ... --dump-format SQLITE` / replication: it mirrors
dumped tables into a local SQLite file. Tested end-to-end against a real temp
database (create table, typed columns, insert, select, persistence) and read
back independently with the stdlib sqlite3 driver.
"""
import os
import sqlite3
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.replication import Replication
class _ReplCase(unittest.TestCase):
def setUp(self):
fd, self.path = tempfile.mkstemp(suffix=".sqlite")
os.close(fd)
os.remove(self.path)
self.rep = Replication(self.path)
def tearDown(self):
try:
del self.rep
except Exception:
pass
if os.path.exists(self.path):
os.remove(self.path)
def _readback(self, sql):
conn = sqlite3.connect(self.path)
try:
return conn.execute(sql).fetchall()
finally:
conn.close()
class TestCreateInsertSelect(_ReplCase):
def test_roundtrip(self):
t = self.rep.createTable("users", [("id", self.rep.INTEGER), ("name", self.rep.TEXT)])
t.insert([1, "admin"])
t.insert([2, "guest"])
self.assertEqual(t.select(), [(1, "admin"), (2, "guest")])
def test_persisted_to_disk(self):
t = self.rep.createTable("t", [("id", self.rep.INTEGER), ("v", self.rep.TEXT)])
t.insert([10, "x"])
# autocommit (isolation_level=None) => visible to an independent connection
self.assertEqual(self._readback("SELECT id, v FROM t"), [(10, "x")])
def test_real_and_blob_types(self):
t = self.rep.createTable("mix", [("r", self.rep.REAL), ("b", self.rep.BLOB)])
t.insert([3.5, b"\x00\x01"])
self.assertEqual(self._readback("SELECT r FROM mix")[0][0], 3.5) # REAL preserved exactly
# BLOB containing a NUL byte must survive intact (a naive str path would truncate at \x00).
# It comes back as a 2-element value (text on py3); assert the NUL didn't truncate it.
blob = self._readback("SELECT b FROM mix")[0][0]
self.assertEqual(len(blob), 2, msg="blob truncated/altered: %r" % (blob,))
def test_null_and_empty_values(self):
t = self.rep.createTable("n", [("id", self.rep.INTEGER), ("v", self.rep.TEXT)])
t.insert([None, ""])
self.assertEqual(self._readback("SELECT id, v FROM n"), [(None, "")])
def test_create_replaces_existing(self):
t1 = self.rep.createTable("dup", [("id", self.rep.INTEGER)])
t1.insert([1])
# createTable drops-if-exists, so the table is fresh
t2 = self.rep.createTable("dup", [("id", self.rep.INTEGER)])
self.assertEqual(t2.select(), [])
if __name__ == "__main__":
unittest.main(verbosity=2)

60
tests/test_safe2bin.py Normal file
View file

@ -0,0 +1,60 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
safecharencode / safechardecode (lib/utils/safe2bin.py).
These make extracted DB values safe to print/store by escaping control and
non-printable characters (tab -> \\t, NUL -> \\x00, ...) and back. They are
applied to dumped data and to values written through the replication writer,
so the escape<->unescape round-trip must be exact.
"""
import os
import random
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.utils.safe2bin import safecharencode, safechardecode
RND = random.Random(99)
class TestKnownEscapes(unittest.TestCase):
CASES = [
(u"normal", u"normal"),
(u"tab\there", u"tab\\there"),
(u"new\nline", u"new\\nline"),
(u"nul\x00byte", u"nul\\x00byte"),
]
def test_encode(self):
for raw, encoded in self.CASES:
self.assertEqual(safecharencode(raw), encoded, msg="safecharencode(%r)" % raw)
def test_plain_text_unchanged(self):
for s in (u"plain", u"abc 123", u"semi;colon", u"a,b,c"):
self.assertEqual(safecharencode(s), s, msg="plain text altered: %r" % s)
class TestRoundTrip(unittest.TestCase):
def test_known_roundtrip(self):
for raw, _ in TestKnownEscapes.CASES:
self.assertEqual(safechardecode(safecharencode(raw)), raw, msg="round-trip %r" % raw)
def test_property_roundtrip(self):
# mix printable + control/non-printable code points
pool = u"abc 123" + u"".join(chr(c) for c in (0, 1, 7, 9, 10, 13, 27, 127))
for _ in range(2000):
s = u"".join(RND.choice(pool) for _ in range(RND.randint(0, 24)))
self.assertEqual(safechardecode(safecharencode(s)), s, msg="round-trip failed for %r" % s)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -0,0 +1,66 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Compiled-regex battery for lib/core/settings.py.
settings.py defines ~40 module-level *_REGEX patterns that drive WAF/error/
charset/IP/title detection. A bad edit to any one of them is a silent failure
(detection just stops firing). This compiles them all and pins the behavior of
the high-traffic detection patterns with positive + negative cases.
"""
import os
import re
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
import lib.core.settings as S
from lib.core.common import extractRegexResult
class TestAllRegexesCompile(unittest.TestCase):
def test_every_regex_constant_compiles(self):
names = [n for n in dir(S) if n.endswith("_REGEX")]
self.assertGreater(len(names), 20, msg="expected many *_REGEX constants")
failures = []
for name in names:
value = getattr(S, name)
if isinstance(value, str):
# some carry a single %s placeholder (e.g. SENSITIVE_DATA_REGEX) - fill it before compiling
candidate = value.replace("%s", "X") if "%s" in value else value
try:
re.compile(candidate)
except re.error as ex:
failures.append("%s: %s" % (name, ex))
self.assertEqual(failures, [], msg="non-compiling regexes: %s" % failures)
class TestDetectionPatterns(unittest.TestCase):
def test_ip_address(self):
self.assertTrue(re.search(S.IP_ADDRESS_REGEX, "connect to 192.168.0.1 now"))
self.assertFalse(re.search(S.IP_ADDRESS_REGEX, "999.999.999.999"))
def test_permission_denied(self):
self.assertEqual(extractRegexResult(S.PERMISSION_DENIED_REGEX, "access denied for user 'x'"),
"access denied")
def test_parameter_splitting(self):
self.assertEqual(re.split(S.PARAMETER_SPLITTING_REGEX, "a,b;c|d"), ["a", "b", "c", "d"])
def test_html_title(self):
self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "<title>Hello</title>"), "Hello")
# case-insensitive tag, first-of-two wins, empty/absent -> None (probed)
self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "<TITLE>x</TITLE>"), "x")
self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "<title>A</title><title>B</title>"), "A")
self.assertIsNone(extractRegexResult(S.HTML_TITLE_REGEX, "<title></title>"))
self.assertIsNone(extractRegexResult(S.HTML_TITLE_REGEX, "no title here"))
if __name__ == "__main__":
unittest.main(verbosity=2)

87
tests/test_sqlparse.py Normal file
View file

@ -0,0 +1,87 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
SQL/string parsing helpers: field splitting and 0-depth (paren+quote aware)
scanning, query cleanup, regex extraction.
Includes regression cases for the quote-awareness bugs fixed previously.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import splitFields, zeroDepthSearch, cleanQuery, extractRegexResult
class TestSplitFields(unittest.TestCase):
CASES = [
("a,b", ["a", "b"]),
("user,password", ["user", "password"]),
("a,b,c", ["a", "b", "c"]),
("a", ["a"]),
("max(a,b)", ["max(a,b)"]), # paren-protected
("max(a, b),c", ["max(a,b)", "c"]), # ', ' normalized; outer split
("COUNT(*),name", ["COUNT(*)", "name"]),
("f(g(x,y),z),h", ["f(g(x,y),z)", "h"]), # nested parens
("'a,b'", ["'a,b'"]), # REGRESSION: comma in single-quoted literal
("'a,b','c|d','e&f'", ["'a,b'", "'c|d'", "'e&f'"]), # REGRESSION
('"x,y",z', ['"x,y"', "z"]), # double-quoted literal
]
def test_table(self):
for inp, expected in self.CASES:
self.assertEqual(splitFields(inp), expected, msg="splitFields(%r)" % inp)
class TestZeroDepthSearch(unittest.TestCase):
def test_quote_awareness(self):
# ' FROM ' inside a literal must NOT be a clause boundary (regression)
self.assertEqual(zeroDepthSearch("SELECT 'x FROM y'", " FROM "), [])
# a real FROM must be found (exactly once here)
self.assertEqual(len(zeroDepthSearch("SELECT a FROM t", " FROM ")), 1)
def test_paren_awareness(self):
self.assertEqual(zeroDepthSearch("a(,)b,c", ","), [5]) # only the depth-0 comma
def test_doctest_vectors(self):
q = "SELECT (SELECT id FROM users WHERE 2>1) AS result FROM DUAL"
hits = zeroDepthSearch(q, "FROM")
self.assertTrue(hits, "no depth-0 FROM found") # guard: avoid a confusing IndexError
self.assertEqual(q[hits[0]:], "FROM DUAL") # outer FROM only
s = "a(b; c),d;e"
hits = zeroDepthSearch(s, "[;, ]")
self.assertTrue(hits)
self.assertEqual(s[hits[0]:], ",d;e") # char-class form
class TestCleanQuery(unittest.TestCase):
def test_keyword_uppercasing(self):
self.assertEqual(cleanQuery("select a from t"), "SELECT a FROM t")
# mixed case keywords get uppercased; non-keyword identifiers are preserved verbatim
self.assertEqual(cleanQuery("seLeCt a fRoM t"), "SELECT a FROM t")
self.assertEqual(cleanQuery("SELECT 1"), "SELECT 1") # already-upper unchanged
def test_idempotent(self):
for q in ["select a from t", "SELECT 1", "select x where y=1 order by z"]:
once = cleanQuery(q)
self.assertEqual(cleanQuery(once), once)
# idempotence alone would pass even if cleanQuery uppercased EVERYTHING; anchor that it
# uppercases keywords but preserves the lowercase identifier
self.assertEqual(cleanQuery("select a from t"), "SELECT a FROM t")
class TestExtractRegexResult(unittest.TestCase):
def test_named_group(self):
self.assertEqual(extractRegexResult(r"id=(?P<result>\d+)", "id=42"), "42")
self.assertIsNone(extractRegexResult(r"id=(?P<result>\d+)", "no match here"))
if __name__ == "__main__":
unittest.main(verbosity=2)

102
tests/test_strings.py Normal file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
String / path / escape helpers.
"""
import os
import random
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import (normalizePath, posixToNtSlashes, ntToPosixSlashes,
isHexEncodedString, decodeStringEscape, encodeStringEscape,
listToStrValue, filterControlChars, safeVariableNaming,
unsafeVariableNaming, longestCommonPrefix, decodeIntToUnicode)
RND = random.Random(7)
class TestPaths(unittest.TestCase):
def test_normalizePath(self):
self.assertEqual(normalizePath("a//b/c"), "a/b/c")
def test_slashes(self):
self.assertEqual(posixToNtSlashes("/a/b"), "\\a\\b")
self.assertEqual(ntToPosixSlashes("a\\b"), "a/b")
def test_slash_roundtrip(self):
for _ in range(500):
s = "/".join(["seg%d" % RND.randint(0, 9) for _ in range(RND.randint(2, 6))])
nt = posixToNtSlashes(s)
# non-identity anchor: the NT form must actually differ (no '/', has '\') -
# otherwise a no-op pair would pass this round-trip
self.assertNotIn("/", nt, msg="posixToNtSlashes left a '/': %r" % nt)
self.assertIn("\\", nt)
self.assertEqual(ntToPosixSlashes(nt), s)
class TestHexDetection(unittest.TestCase):
CASES = [("0x4142", True), ("4142", True), ("zz", False), ("0xZZ", False), ("", False)]
def test_isHexEncodedString(self):
for v, exp in self.CASES:
self.assertEqual(bool(isHexEncodedString(v)), exp, msg="isHexEncodedString(%r)" % v)
class TestStringEscape(unittest.TestCase):
def test_known(self):
self.assertEqual(decodeStringEscape("a\\tb"), "a\tb")
self.assertEqual(encodeStringEscape("a\tb"), "a\\tb")
def test_roundtrip_property(self):
ctrl = "\t\n\r\\abc 123"
for _ in range(2000):
s = "".join(RND.choice(ctrl) for _ in range(RND.randint(0, 20)))
self.assertEqual(decodeStringEscape(encodeStringEscape(s)), s)
class TestVariableNaming(unittest.TestCase):
def test_transform_is_not_identity(self):
# safeVariableNaming hex-encodes non-identifier-safe names behind an EVAL_ prefix;
# pin the exact form so the round-trip below can't be satisfied by no-op functions
self.assertEqual(safeVariableNaming("a.b"), "EVAL_612e62") # 612e62 == hex("a.b")
self.assertNotEqual(safeVariableNaming("weird name"), "weird name")
def test_roundtrip(self):
for ident in ["a.b", "schema.table", "x", "weird name", "a-b.c"]:
encoded = safeVariableNaming(ident)
if any(c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" for c in ident):
self.assertNotEqual(encoded, ident, msg="unsafe ident %r was not transformed" % ident)
self.assertEqual(unsafeVariableNaming(encoded), ident)
class TestMiscStrings(unittest.TestCase):
def test_listToStrValue(self):
self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3")
def test_filterControlChars(self):
self.assertEqual(filterControlChars("a\x07b"), "a b")
def test_longestCommonPrefix(self):
self.assertEqual(longestCommonPrefix("abcx", "abcy"), "abc")
self.assertEqual(longestCommonPrefix("abc", "xyz"), "")
def test_decodeIntToUnicode(self):
# single-byte code points map to their char
self.assertEqual(decodeIntToUnicode(65), u"A")
self.assertEqual(decodeIntToUnicode(97), u"a")
# NOTE: >255 ints are interpreted as a multi-byte sequence (not a Unicode code point),
# e.g. 0x2122 -> bytes 0x21 0x22 -> '!"' (documents actual behavior, not an assumption)
self.assertEqual(decodeIntToUnicode(0x2122), u'!"')
if __name__ == "__main__":
unittest.main(verbosity=2)

125
tests/test_tamper.py Normal file
View file

@ -0,0 +1,125 @@
#!/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)

70
tests/test_targeturl.py Normal file
View file

@ -0,0 +1,70 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Target URL parsing (lib/core/common.py parseTargetUrl).
parseTargetUrl reads conf.url and populates conf.hostname / conf.port /
conf.scheme / conf.path - the values every subsequent request is built from. A
wrong default port or dropped scheme here misdirects the entire scan, so the
scheme/default-port/explicit-port/path cases are pinned.
(Inline URL credentials user:pw@host are intentionally not covered - sqlmap
uses --auth-cred for that and does not parse them out of conf.url.)
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import parseTargetUrl
from lib.core.data import conf
def _parse(url):
conf.url = url
parseTargetUrl()
return conf.hostname, conf.port, conf.scheme, conf.path
class TestScheme(unittest.TestCase):
def test_http(self):
host, port, scheme, _ = _parse("http://host/p?id=1")
self.assertEqual((host, scheme), ("host", "http"))
def test_https(self):
_, _, scheme, _ = _parse("https://host/p")
self.assertEqual(scheme, "https")
class TestDefaultPorts(unittest.TestCase):
def test_http_default_80(self):
self.assertEqual(_parse("http://h/")[1], 80)
def test_https_default_443(self):
self.assertEqual(_parse("https://h/")[1], 443)
def test_no_trailing_slash(self):
host, port, scheme, _ = _parse("http://h")
self.assertEqual((host, port), ("h", 80))
class TestExplicitPort(unittest.TestCase):
def test_explicit_port(self):
host, port, scheme, _ = _parse("https://example.com:8443/x")
self.assertEqual((host, port, scheme), ("example.com", 8443, "https"))
class TestPath(unittest.TestCase):
def test_path_extracted(self):
self.assertEqual(_parse("http://host/some/path?q=1")[3], "/some/path")
if __name__ == "__main__":
unittest.main(verbosity=2)

74
tests/test_texthelpers.py Normal file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Text-processing helpers in lib/core/common.py:
normalizeUnicode (accent folding), filterStringValue (charset whitelist),
parseFilePaths (absolute-path harvesting from error pages -> kb.absFilePaths),
getSafeExString (safe exception rendering).
parseFilePaths in particular feeds path disclosure / file-read targeting, so
its extraction is pinned with realistic PHP/ASP error strings.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import normalizeUnicode, filterStringValue, parseFilePaths, getSafeExString
from lib.core.data import kb
class TestNormalizeUnicode(unittest.TestCase):
def test_strips_accents(self):
self.assertEqual(normalizeUnicode(u"caf\xe9 r\xe9sum\xe9"), u"cafe resume")
def test_ascii_unchanged(self):
self.assertEqual(normalizeUnicode(u"plain ascii 123"), u"plain ascii 123")
class TestFilterStringValue(unittest.TestCase):
def test_keep_lowercase(self):
self.assertEqual(filterStringValue("abc123!@#", r"[a-z]"), "abc")
def test_keep_digits(self):
self.assertEqual(filterStringValue("a1b2c3", r"[0-9]"), "123")
def test_all_match(self):
self.assertEqual(filterStringValue("abc", r"[a-z]"), "abc")
class TestParseFilePaths(unittest.TestCase):
def setUp(self):
kb.absFilePaths = set()
def test_unix_paths_from_php_error(self):
parseFilePaths("Warning: include(/var/www/html/config.php) failed "
"to open stream in /var/www/html/index.php on line 5")
self.assertIn("/var/www/html/config.php", kb.absFilePaths)
self.assertIn("/var/www/html/index.php", kb.absFilePaths)
def test_windows_path(self):
# exact full path (not a substring) - a truncated harvest is a real defect for file-read targeting
parseFilePaths("Fatal error in C:\\inetpub\\wwwroot\\app\\index.asp on line 1")
self.assertIn("C:\\inetpub\\wwwroot\\app\\index.asp", kb.absFilePaths,
msg="windows path not harvested in full: %s" % kb.absFilePaths)
class TestGetSafeExString(unittest.TestCase):
def test_format(self):
self.assertEqual(getSafeExString(ValueError("boom")), u"ValueError: boom")
def test_runtime_error(self):
# RuntimeError keeps its name across py2/py3 (unlike IOError, which aliases to OSError on py3)
self.assertEqual(getSafeExString(RuntimeError("oops")), u"RuntimeError: oops")
if __name__ == "__main__":
unittest.main(verbosity=2)

107
tests/test_union_engine.py Normal file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
The UNION-based column-count detection engine (lib/techniques/union/test.py).
_findUnionCharCount discovers how many columns a UNION injection needs. Its
fastest path is the ORDER BY technique: a valid target accepts ORDER BY 1..N and
errors on ORDER BY N+1, so it binary-searches for N. We drive the REAL function
against a mock oracle (Request.queryPage replaced) that errors once the requested
column index exceeds a known true count - exercising the actual detection +
binary search with no live target.
This requires the full injection context (conf.parameters / conf.paramDict /
kb.injection) because column detection builds real payloads via agent.payload.
"""
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.data import conf, kb
from lib.core.enums import PAYLOAD, PLACE
from lib.request.connect import Connect
import lib.techniques.union.test as ut
MARKER = "MARKER42"
VALID_PAGE = "<html>results %s</html>" % MARKER
_CONF = {"string": MARKER, "notString": None, "regexp": None, "code": None,
"uCols": None, "uColsStart": 1, "uColsStop": 50, "base64Parameter": ()}
_KB = {"heavilyDynamic": False, "errorIsNone": False, "futileUnion": False,
"uChar": "NULL", "forceWhere": None}
class TestOrderByColumnCount(unittest.TestCase):
def setUp(self):
self._sc = {k: conf.get(k) for k in _CONF}
self._sk = {k: kb.get(k) for k in _KB}
self._sp = (conf.get("parameters"), conf.get("paramDict"))
self._sqp = Connect.queryPage
self._stmpl = kb.get("pageTemplate")
self._sinj = (kb.injection.place, kb.injection.parameter)
for k, v in _CONF.items():
conf[k] = v
for k, v in _KB.items():
kb[k] = v
conf.parameters = {PLACE.GET: "id=1"}
conf.paramDict = {PLACE.GET: {"id": "1"}}
kb.pageTemplate = VALID_PAGE
kb.injection.place = None
kb.injection.parameter = None
set_dbms("MySQL")
def tearDown(self):
for k, v in self._sc.items():
conf[k] = v
for k, v in self._sk.items():
kb[k] = v
conf.parameters, conf.paramDict = self._sp
kb.pageTemplate = self._stmpl
kb.injection.place, kb.injection.parameter = self._sinj
Connect.queryPage = self._sqp
ut.Request.queryPage = self._sqp
def _detect(self, true_count):
def oracle(payload=None, place=None, content=False, raise404=True, **kwargs):
m = re.search(r"ORDER BY (\d+)", payload or "")
cols = int(m.group(1)) if m else 1
if cols <= true_count:
page = VALID_PAGE
else:
page = "<html>Unknown column '%d' in 'order clause'</html>" % cols
return (page, {}, 200) if content else True
Connect.queryPage = staticmethod(oracle)
ut.Request.queryPage = staticmethod(oracle)
kb.orderByColumns = None
return ut._findUnionCharCount("-- -", PLACE.GET, "id", "1", "", "", PAYLOAD.WHERE.ORIGINAL)
def test_detect_single_column(self):
self.assertEqual(self._detect(1), 1)
def test_detect_small(self):
self.assertEqual(self._detect(3), 3)
def test_detect_medium(self):
self.assertEqual(self._detect(7), 7)
def test_detect_larger(self):
self.assertEqual(self._detect(12), 12)
def test_detect_beyond_first_step(self):
# > ORDER_BY_STEP (10): forces the expand-then-bisect branch
self.assertEqual(self._detect(25), 25)
if __name__ == "__main__":
unittest.main(verbosity=2)

80
tests/test_urls.py Normal file
View file

@ -0,0 +1,80 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
URL encode/decode round-trips, parameter parsing, same-host checks.
"""
import os
import random
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import urldecode, urlencode, paramToDict, checkSameHost
from lib.core.enums import PLACE
RND = random.Random(11)
class TestUrlCoding(unittest.TestCase):
def test_known(self):
self.assertEqual(urldecode("a%20b"), u"a b")
self.assertEqual(urlencode("a b&c"), "a%20b&c")
def test_encode_is_not_identity(self):
# anchor so the round-trip property below can't pass with no-op functions:
# special chars MUST be percent-encoded
encoded = urlencode("a b&c=d", safe="")
self.assertNotIn(" ", encoded)
self.assertNotIn("&", encoded)
self.assertEqual(encoded, "a%20b%26c%3Dd")
def test_roundtrip_property(self):
import string
# NOTE: urldecode() by default preserves URL-structural chars (?, &, =, +, ;) so a full
# round-trip needs convall=True; '+' still excluded (form-encoding maps it to space).
alphabet = string.ascii_letters + string.digits + " &=?/#@:,'\""
for _ in range(2000):
s = "".join(RND.choice(alphabet) for _ in range(RND.randint(0, 25)))
roundtripped = urldecode(urlencode(s, safe=""), convall=True)
self.assertEqual(roundtripped, s, msg="roundtrip %r" % s)
class TestParamToDict(unittest.TestCase):
def test_get(self):
d = paramToDict(PLACE.GET, "a=1&b=2&c=3")
self.assertEqual(d.get("a"), "1")
self.assertEqual(d.get("b"), "2")
self.assertEqual(d.get("c"), "3")
def test_get_single(self):
d = paramToDict(PLACE.GET, "id=42")
self.assertEqual(d.get("id"), "42")
class TestSameHost(unittest.TestCase):
def test_same(self):
self.assertTrue(checkSameHost("http://h/a", "http://h/b"))
self.assertTrue(checkSameHost("http://h:80/a", "http://h:80/b"))
def test_www_prefix_is_same(self):
# documented behavior: a leading www. is normalized away
self.assertTrue(checkSameHost("http://example.com/a", "http://www.example.com/b"))
def test_different_host_is_false(self):
# discriminating: an always-True implementation must fail here
self.assertFalse(checkSameHost("http://h/a", "http://other/b"))
self.assertFalse(checkSameHost("http://example.com/a", "http://evil.com/b"))
def test_one_none_is_false(self):
self.assertFalse(checkSameHost("http://h/a", None))
if __name__ == "__main__":
unittest.main(verbosity=2)

117
tests/test_utils.py Normal file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Core utility helpers: constant-time compare, numeric checks, safe formatting,
list/value normalization, randomness generators.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import (safeCompareStrings, isDigit, isNumber, safeStringFormat,
filterNone, flattenValue, isListLike, unArrayizeValue,
arrayizeValue, randomStr, randomInt)
class TestSafeCompareStrings(unittest.TestCase):
def test_known(self):
self.assertTrue(safeCompareStrings("abc", "abc"))
self.assertFalse(safeCompareStrings("abc", "abd"))
self.assertFalse(safeCompareStrings("test", None))
self.assertTrue(safeCompareStrings(None, None))
self.assertFalse(safeCompareStrings("a", "ab")) # different length
def test_property(self):
for s in ["", "a", "secret", "p@ss w0rd", "x" * 100]:
self.assertTrue(safeCompareStrings(s, s))
self.assertFalse(safeCompareStrings(s, s + "x"))
class TestNumericChecks(unittest.TestCase):
def test_isDigit(self):
for v, exp in [("123", True), ("0", True), ("12a", False), ("", False), ("-1", False)]:
self.assertEqual(bool(isDigit(v)), exp, msg="isDigit(%r)" % v)
def test_isNumber(self):
for v, exp in [("123", True), ("1.5", True), ("1e3", True), ("abc", False), ("", False)]:
self.assertEqual(bool(isNumber(v)), exp, msg="isNumber(%r)" % v)
class TestSafeStringFormat(unittest.TestCase):
def test_basic(self):
self.assertEqual(safeStringFormat("%s-%d", ("a", 5)), "a-5")
self.assertEqual(safeStringFormat("%s/%s", ("x", "y")), "x/y")
def test_survives_percent_in_value(self):
# the WHOLE point of safeStringFormat over plain `%`: a '%' inside an argument (common in
# payloads/URL-encoded values) must not blow up or be misread as a format spec.
# Plain "x=%s" % ("100%done",) would raise on re-evaluation; safeStringFormat must not.
self.assertEqual(safeStringFormat("x=%s", ("100%done",)), "x=100%done")
class TestListValueHelpers(unittest.TestCase):
def test_filterNone(self):
self.assertEqual(filterNone([1, None, 2, 0, "", None]), [1, 2, 0])
self.assertEqual(filterNone([]), [])
self.assertEqual(filterNone([None, None]), [])
def test_flattenValue(self):
self.assertEqual(list(flattenValue([[1, 2], [3, [4]]])), [1, 2, 3, 4])
self.assertEqual(list(flattenValue([])), [])
self.assertEqual(list(flattenValue([1])), [1])
def test_isListLike(self):
from lib.core.datatype import OrderedSet
from lib.core.bigarray import BigArray
# isListLike is sqlmap-specific: it must recognize sqlmap's own list-like containers
# (OrderedSet, BigArray), not just builtin list/tuple - that's why it's not isinstance(list)
self.assertTrue(isListLike([1]))
self.assertTrue(isListLike((1,)))
self.assertTrue(isListLike(OrderedSet([1, 2])))
self.assertTrue(isListLike(BigArray([1])))
# and must reject str (the classic trap) and dict
self.assertFalse(isListLike("string"))
self.assertFalse(isListLike({"a": 1}))
def test_arrayize_roundtrip(self):
self.assertEqual(unArrayizeValue([5]), 5)
self.assertIsNone(unArrayizeValue([]))
self.assertEqual(unArrayizeValue(7), 7)
self.assertEqual(arrayizeValue(5), [5])
self.assertEqual(arrayizeValue([5]), [5])
class TestRandomGenerators(unittest.TestCase):
def test_randomStr_length_and_alphabet(self):
for n in (1, 4, 16, 50):
self.assertEqual(len(randomStr(n)), n)
for _ in range(200):
self.assertTrue(all("a" <= c <= "z" for c in randomStr(20, lowercase=True)))
alpha = list("ABC")
for _ in range(200):
self.assertTrue(all(c in alpha for c in randomStr(20, alphabet=alpha)))
def test_randomStr_is_actually_random(self):
# guard against a hardcoded/constant return: 20-char strings must (essentially) never collide
samples = set(randomStr(20) for _ in range(100))
self.assertEqual(len(samples), 100, msg="randomStr produced collisions - not random?")
def test_randomInt_digits(self):
for n in (1, 3, 6):
lo, hi = 10 ** (n - 1), 10 ** n
for _ in range(200):
v = randomInt(n)
self.assertEqual(len(str(v)), n) # exactly n digits
self.assertTrue(lo <= v < hi, msg="randomInt(%d)=%d out of [%d,%d)" % (n, v, lo, hi))
if __name__ == "__main__":
unittest.main(verbosity=2)

96
tests/test_wordlist.py Normal file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Wordlist iterator (lib/core/wordlist.py).
Backs dictionary attacks (--common-tables, password cracking, brute force): a
lazy iterator that streams words across one or more files (and zip archives)
without loading them into RAM. Tested for ordering, multi-file chaining,
rewind, and end-of-stream behavior over real temp files.
"""
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.wordlist import Wordlist
def _mkfile(lines):
fd, path = tempfile.mkstemp()
os.write(fd, ("\n".join(lines) + "\n").encode("utf-8"))
os.close(fd)
return path
def _w(s):
# Wordlist yields native str on py2 but bytes on py3 (words are fed straight into HTTP payloads)
return s.encode("utf-8") if sys.version_info[0] >= 3 else s
def _drain(w):
out = []
try:
while True:
out.append(next(w))
except StopIteration:
pass
return out
class TestWordlist(unittest.TestCase):
def setUp(self):
self.paths = []
self.wordlists = []
def tearDown(self):
for w in self.wordlists: # close open file handles (else ResourceWarning on py3)
try:
w.closeFP()
except Exception:
pass
for p in self.paths:
if os.path.exists(p):
os.remove(p)
def _mk(self, lines):
p = _mkfile(lines)
self.paths.append(p)
return p
def _wl(self, files):
w = Wordlist(files)
self.wordlists.append(w)
return w
def test_single_file_order(self):
w = self._wl([self._mk(["alpha", "beta", "gamma"])])
self.assertEqual(_drain(w), [_w("alpha"), _w("beta"), _w("gamma")])
def test_multiple_files_chained(self):
w = self._wl([self._mk(["a", "b"]), self._mk(["c", "d"])])
self.assertEqual(_drain(w), [_w("a"), _w("b"), _w("c"), _w("d")])
def test_rewind_restarts(self):
w = self._wl([self._mk(["one", "two"])])
self.assertEqual(next(w), _w("one"))
self.assertEqual(next(w), _w("two"))
w.rewind()
self.assertEqual(next(w), _w("one"))
def test_end_raises_stopiteration(self):
w = self._wl([self._mk(["only"])])
self.assertEqual(next(w), _w("only"))
self.assertRaises(StopIteration, lambda: next(w))
if __name__ == "__main__":
unittest.main(verbosity=2)