mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-27 20:11:02 +00:00
Adding unit tests
This commit is contained in:
parent
48b915b5ee
commit
3816df1241
39 changed files with 3501 additions and 2 deletions
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
6
tests/__init__.py
Normal 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
89
tests/_testutils.py
Normal 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
86
tests/test_agent.py
Normal 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
95
tests/test_bigarray.py
Normal 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
71
tests/test_charset.py
Normal 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
67
tests/test_cloak.py
Normal 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)
|
||||
76
tests/test_common_helpers.py
Normal file
76
tests/test_common_helpers.py
Normal 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
132
tests/test_comparison.py
Normal 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
141
tests/test_convert.py
Normal 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"a’b™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
124
tests/test_datafiles.py
Normal 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
96
tests/test_datatypes.py
Normal 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
81
tests/test_decodepage.py
Normal 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
107
tests/test_dialect.py
Normal 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
88
tests/test_dicts.py
Normal 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
75
tests/test_encoding.py
Normal 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
113
tests/test_error_engine.py
Normal 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
105
tests/test_hash.py
Normal 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
129
tests/test_hashdb.py
Normal 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)
|
||||
78
tests/test_identifiers_output.py
Normal file
78
tests/test_identifiers_output.py
Normal 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)
|
||||
153
tests/test_inference_engine.py
Normal file
153
tests/test_inference_engine.py
Normal 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
125
tests/test_misc.py
Normal 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
85
tests/test_pagecontent.py
Normal 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)
|
||||
157
tests/test_payload_marking.py
Normal file
157
tests/test_payload_marking.py
Normal 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)
|
||||
110
tests/test_payloads_structure.py
Normal file
110
tests/test_payloads_structure.py
Normal 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
87
tests/test_replication.py
Normal 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
60
tests/test_safe2bin.py
Normal 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)
|
||||
66
tests/test_settings_regex.py
Normal file
66
tests/test_settings_regex.py
Normal 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
87
tests/test_sqlparse.py
Normal 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
102
tests/test_strings.py
Normal 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
125
tests/test_tamper.py
Normal 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
70
tests/test_targeturl.py
Normal 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
74
tests/test_texthelpers.py
Normal 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
107
tests/test_union_engine.py
Normal 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
80
tests/test_urls.py
Normal 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
117
tests/test_utils.py
Normal 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
96
tests/test_wordlist.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue