Skip to content

Commit be284e9

Browse files
committed
Adding more unittests
1 parent c210dac commit be284e9

4 files changed

Lines changed: 330 additions & 2 deletions

File tree

data/txt/sha256sums.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch
188188
48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py
189189
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
190190
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
191-
4d42429d71efaf20f17cc7709b0da60f661c7cd100855c7b71d6248d5d905319 lib/core/settings.py
191+
03034e80de6b81ec5d5482f8c4dff1722f636f09e226f42b6849e78164da3682 lib/core/settings.py
192192
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
193193
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
194194
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
@@ -589,6 +589,7 @@ caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_mis
589589
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
590590
4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py
591591
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py
592+
5dc46919f971f89a3073118ec00bf420cc9cecf0b072b2f896df2f860e87adec tests/test_property.py
592593
5c95e7863190e440234f231864fb1219c35207132762858cc95181c57086bafc tests/test_replication.py
593594
67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py
594595
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py

lib/core/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.6.113"
23+
VERSION = "1.10.6.114"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

tests/_testutils.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,66 @@ def set_dbms(name):
8787
from lib.core.data import kb
8888
kb.stickyDBMS = False
8989
Backend.forceDbms(name)
90+
91+
92+
# --- property/fuzz testing harness (shared so individual test files don't each reinvent it) ---
93+
94+
_PROPERTY_BASE = 0x51A1
95+
96+
97+
class Rng(object):
98+
"""Deterministic, cross-version-identical PRNG (a pure-integer LCG, no global state).
99+
100+
sqlmap runs on Python 2.7 and 3.x, whose stdlib `random` yield DIFFERENT sequences
101+
for the same seed - and `random.Random` instance methods are not unified by
102+
patch.unisonRandom() (which only patches the module-level random.choice/randint/
103+
sample/seed). Property tests need inputs that are byte-for-byte identical on every
104+
interpreter so a CI-only failure reproduces everywhere; integer math is identical
105+
across versions, so this LCG (same constants as unisonRandom) guarantees it by
106+
construction. Draw ONLY through these methods - never random.random()/shuffle()/etc.
107+
"""
108+
109+
def __init__(self, seed):
110+
self.x = seed & 0xFFFFFF
111+
112+
def _next(self):
113+
self.x = (1140671485 * self.x + 128201163) % (2 ** 24)
114+
return self.x
115+
116+
def randint(self, a, b):
117+
return a + self._next() % (b - a + 1)
118+
119+
def choice(self, seq):
120+
return seq[self.randint(0, len(seq) - 1)]
121+
122+
def sample(self, seq, k):
123+
# Note: with replacement (matches unisonRandom's _sample); fine for input generation
124+
return [self.choice(seq) for _ in range(k)]
125+
126+
def blob(self, n):
127+
return bytes(bytearray(self.randint(0, 255) for _ in range(n)))
128+
129+
130+
def _label_offset(label):
131+
# stable across versions/runs (unlike hash(), which varies with PYTHONHASHSEED): just sum bytes
132+
return sum(bytearray((label or "").encode("utf-8"))) * 7919
133+
134+
135+
def for_all(testcase, generator, prop, n=400, label=""):
136+
"""Property runner: draw `n` cases from generator(rng) and assert prop(case) holds.
137+
138+
`prop` passes by returning True/None, fails by returning False or raising. On any
139+
failure the EXACT offending input and its case index are reported; the same input
140+
is reproducible (and identical on every interpreter) via Rng(seed_for(label, i)).
141+
"""
142+
base = _PROPERTY_BASE + _label_offset(label)
143+
for i in range(n):
144+
case = generator(Rng(base + i))
145+
try:
146+
ok = prop(case)
147+
except Exception as ex:
148+
testcase.fail("%s: raised %r on input %r (case %d)" % (label or "property", ex, case, i))
149+
return
150+
if ok is False:
151+
testcase.fail("%s: property does not hold on input %r (case %d)" % (label or "property", case, i))
152+
return

tests/test_property.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
5+
See the file 'LICENSE' for copying permission
6+
7+
Property/fuzz tests for the pure parsers and transforms. Where the other test
8+
files pin specific examples, these assert INVARIANTS over hundreds of randomized
9+
(but deterministic, cross-version-identical - see _testutils.Rng) inputs, which is
10+
the cheap net for the edge-bug class that example tests miss (commas inside quoted
11+
literals / nested parens, NUL / 0xff / astral code points in codecs, etc.).
12+
13+
Property families:
14+
- codec/serializer pairs round-trip: decode(encode(x)) == x
15+
- structure transforms preserve their contract (flat/de-arrayized/permutation)
16+
- string transforms hold their stated invariant (ASCII-only, no newlines, ...)
17+
- random helpers respect length / alphabet / range bounds
18+
- splitFields/zeroDepthSearch partition faithfully and never cut inside a group
19+
- a batch of transforms never raise on arbitrary input
20+
21+
On failure _testutils.for_all prints the exact offending input + its case index so
22+
it reproduces on any interpreter.
23+
"""
24+
25+
import os
26+
import string
27+
import sys
28+
import unittest
29+
30+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31+
from _testutils import bootstrap, for_all, set_dbms
32+
bootstrap()
33+
34+
from extra.cloak.cloak import cloak, decloak
35+
from lib.core.common import (escapeJsonValue, filterStringValue, flattenValue, isListLike, normalizeUnicode,
36+
prioritySortColumns, randomInt, randomRange, randomStr, safeSQLIdentificatorNaming,
37+
sanitizeStr, splitFields, unArrayizeValue, unsafeSQLIdentificatorNaming, urldecode,
38+
urlencode, zeroDepthSearch)
39+
from lib.core.convert import (base64pickle, base64unpickle, decodeBase64, decodeHex, dejsonize, encodeBase64,
40+
encodeHex, getBytes, getConsoleLength, getOrds, getText, htmlEscape, htmlUnescape,
41+
jsonize, stdoutEncode)
42+
from lib.core.data import kb
43+
from lib.utils.safe2bin import safecharencode
44+
45+
46+
# --- input strategies (draw ONLY through rng: randint / choice / sample / blob) ---
47+
48+
# deliberately loaded with structural metacharacters + tricky code points
49+
_TEXT = [u"a", u"Z", u"7", u" ", u",", u"'", u'"', u"(", u")", u"\\", u";",
50+
u"\n", u"\t", u"\x00", u"\x7f", u"\xe9", u"\u0107", u"\u4e2d", u"\U0001F600", u" FROM "]
51+
52+
53+
def gen_text(rng):
54+
return u"".join(rng.choice(_TEXT) for _ in range(rng.randint(0, 24)))
55+
56+
57+
def gen_ascii(rng):
58+
return u"".join(rng.choice(string.printable) for _ in range(rng.randint(0, 20)))
59+
60+
61+
def gen_blob(rng):
62+
return rng.blob(rng.randint(0, 32))
63+
64+
65+
def gen_json(rng):
66+
# JSON-safe only: tuples become lists and non-str keys are coerced, so exclude them here
67+
if rng.randint(0, 4) == 0:
68+
return [gen_json(rng) for _ in range(rng.randint(0, 3))]
69+
if rng.randint(0, 4) == 0:
70+
return dict((u"k%d" % j, gen_json(rng)) for j in range(rng.randint(0, 3)))
71+
return rng.choice([0, 1, -1, 2 ** 31, 1.5, -0.25, True, False, None, u"", u"x", u"\u0107", u'a"b,c'])
72+
73+
74+
def gen_pickle(rng):
75+
kind = rng.randint(0, 9)
76+
if kind < 5:
77+
return rng.choice([0, -7, 2 ** 40, 3.5, True, False, None, u"\u0107x", b"\x00\xff", u""])
78+
if kind < 7:
79+
return [gen_pickle(rng) for _ in range(rng.randint(0, 3))]
80+
if kind < 8:
81+
return tuple(gen_pickle(rng) for _ in range(rng.randint(0, 3)))
82+
if kind < 9:
83+
return set(rng.choice([1, 2, 3, u"a", u"b"]) for _ in range(rng.randint(0, 3)))
84+
return dict((u"k%d" % j, gen_pickle(rng)) for j in range(rng.randint(0, 2)))
85+
86+
87+
def gen_columns(rng):
88+
return [rng.choice([u"id", u"userid", u"name", u"password", u"a", u"created_id", u"x_id_y", u"data"])
89+
for _ in range(rng.randint(0, 6))]
90+
91+
92+
def gen_ident(rng):
93+
# clean (round-trippable) identifier names: letters/digits/underscore, optional dot/space
94+
chars = string.ascii_letters + string.digits + u"_"
95+
name = u"".join(rng.choice(chars) for _ in range(rng.randint(1, 10)))
96+
if rng.randint(0, 3) == 0:
97+
name += rng.choice([u".col", u" alias", u"_2"])
98+
return name
99+
100+
101+
# well-formed field lists: balanced parens, properly closed/escaped quotes
102+
_TOKENS = [u"foo", u"bar", u"id", u"a b", u"1", u"*", u"max(a)", u"COALESCE(a, b, c)", u"func(x, y)"]
103+
_QUOTED = [u"a,b", u"x, y", u"f(1, 2)", u"o''k", u"plain", u""]
104+
105+
106+
def gen_sql_fields(rng):
107+
parts = []
108+
for _ in range(rng.randint(1, 5)):
109+
t = rng.randint(0, 9)
110+
if t < 5:
111+
parts.append(rng.choice(_TOKENS))
112+
elif t < 8:
113+
q = rng.choice([u"'", u'"'])
114+
parts.append(q + rng.choice(_QUOTED) + q)
115+
else:
116+
parts.append(u"g(%s, %s)" % (rng.choice(_TOKENS), rng.choice(_TOKENS)))
117+
return u", ".join(parts)
118+
119+
120+
class TestCodecRoundTrips(unittest.TestCase):
121+
def test_base64(self):
122+
for_all(self, gen_blob, lambda b: decodeBase64(encodeBase64(b)) == b, label="base64")
123+
124+
def test_hex(self):
125+
for_all(self, gen_blob, lambda b: decodeHex(encodeHex(b)) == b, label="hex")
126+
127+
def test_getbytes_gettext(self):
128+
# unsafe=False -> plain UTF-8 (no \xNN escape interpretation), so it is a clean round-trip
129+
for_all(self, gen_text, lambda s: getText(getBytes(s, unsafe=False)) == s, label="bytes-text")
130+
131+
def test_json(self):
132+
for_all(self, gen_json, lambda v: dejsonize(jsonize(v)) == v, label="json")
133+
134+
def test_pickle(self):
135+
for_all(self, gen_pickle, lambda v: base64unpickle(base64pickle(v)) == v, label="pickle")
136+
137+
def test_html_escape(self):
138+
for_all(self, gen_text, lambda s: htmlUnescape(htmlEscape(s)) == s, label="html")
139+
140+
def test_cloak(self):
141+
for_all(self, gen_blob, lambda b: decloak(data=cloak(data=b)) == b, label="cloak")
142+
143+
144+
class TestStructureTransforms(unittest.TestCase):
145+
def test_unarrayize_never_listlike(self):
146+
# the whole point of unArrayizeValue is that the result is a scalar, never a list/tuple
147+
# (gen_pickle includes sets - they used to crash here; see test_unarrayize_set regression)
148+
for_all(self, gen_pickle, lambda v: not isListLike(unArrayizeValue(v)), label="unarrayize")
149+
150+
def test_flatten_is_flat(self):
151+
for_all(self, gen_pickle, lambda v: all(not isListLike(x) for x in flattenValue([v])), label="flatten")
152+
153+
def test_unarrayize_set(self):
154+
# regression: a 1-element set is list-like but not subscriptable; unArrayizeValue must
155+
# de-arrayize it rather than crash on value[0]
156+
self.assertEqual(unArrayizeValue(set(["x"])), "x")
157+
self.assertEqual(unArrayizeValue(set()), None)
158+
self.assertEqual(unArrayizeValue(["1"]), "1") # ordinary fast-path still works
159+
160+
def test_prioritysort_is_permutation(self):
161+
# sorting must not invent/drop columns, and must be idempotent
162+
def prop(cols):
163+
out = prioritySortColumns(cols)
164+
return sorted(out) == sorted(cols) and prioritySortColumns(out) == out
165+
for_all(self, gen_columns, prop, label="prioritysort")
166+
167+
168+
class TestStringTransforms(unittest.TestCase):
169+
def test_normalize_unicode_is_ascii(self):
170+
for_all(self, gen_text, lambda s: all(ord(c) < 128 for c in normalizeUnicode(s)), label="normalize-ascii")
171+
172+
def test_sanitizestr_strips_newlines(self):
173+
for_all(self, gen_text, lambda s: "\n" not in sanitizeStr(s) and "\r" not in sanitizeStr(s), label="sanitizestr")
174+
175+
def test_filterstringvalue_charset(self):
176+
allowed = set("0123456789abcdef")
177+
for_all(self, gen_text, lambda s: set(filterStringValue(s, r"[0-9a-f]")) <= allowed, label="filterstring")
178+
179+
def test_escapejson_no_control_char(self):
180+
# control chars and bare quotes must be escaped away (output is JSON-string-body safe re: those)
181+
for_all(self, gen_text, lambda s: all(c >= " " for c in escapeJsonValue(s)), label="escapejson-invariant")
182+
183+
def test_escapejson_json_roundtrip(self):
184+
# escapeJsonValue(s) embedded in a JSON string must parse back to s - for ALL text,
185+
# including backslash (the F1 fix; this used to fail on '\')
186+
import json
187+
for_all(self, gen_text, lambda s: json.loads(u'"%s"' % escapeJsonValue(s)) == s, label="escapejson-roundtrip")
188+
189+
def test_escapejson_backslash(self):
190+
# regression for F1: backslash is now escaped, so the round-trip holds
191+
import json
192+
self.assertEqual(json.loads(u'"%s"' % escapeJsonValue(u"a\\b")), u"a\\b")
193+
194+
def test_getords_length(self):
195+
for_all(self, gen_text, lambda s: len(getOrds(s)) == len(s) and all(isinstance(o, int) for o in getOrds(s)), label="getords")
196+
197+
def test_consolelength_ascii(self):
198+
for_all(self, gen_ascii, lambda s: getConsoleLength(s) == len(s), label="consolelength")
199+
200+
201+
class TestRandomHelpers(unittest.TestCase):
202+
def test_randomstr_length_and_alphabet(self):
203+
for_all(self, lambda r: r.randint(0, 16),
204+
lambda n: len(randomStr(n)) == n and set(randomStr(n)) <= set(string.ascii_letters), label="randomstr")
205+
206+
def test_randomstr_lowercase(self):
207+
for_all(self, lambda r: r.randint(0, 16),
208+
lambda n: set(randomStr(n, lowercase=True)) <= set(string.ascii_lowercase), label="randomstr-lower")
209+
210+
def test_randomint_digits(self):
211+
for_all(self, lambda r: r.randint(1, 8), lambda n: len(str(randomInt(n))) == n, label="randomint")
212+
213+
def test_randomrange_bounds(self):
214+
def prop(_):
215+
a = _[0]
216+
b = _[0] + _[1]
217+
return a <= randomRange(a, b) <= b
218+
for_all(self, lambda r: (r.randint(-50, 50), r.randint(0, 100)), prop, label="randomrange")
219+
220+
221+
class TestSplitterInvariants(unittest.TestCase):
222+
def test_reconstruction(self):
223+
# pure partition identity: rejoining the 0-depth split must reproduce the (space-normalized) input
224+
for_all(self, gen_text, lambda s: u",".join(splitFields(s)) == s.replace(", ", ","), label="split-reconstruct-text")
225+
for_all(self, gen_sql_fields, lambda s: u",".join(splitFields(s)) == s.replace(", ", ","), label="split-reconstruct-sql")
226+
227+
def test_never_cuts_inside_parens(self):
228+
# on well-formed input no field may carry unbalanced parens (i.e. a split never lands inside a group)
229+
for_all(self, gen_sql_fields, lambda s: all(f.count(u"(") == f.count(u")") for f in splitFields(s)), label="split-balanced")
230+
231+
def test_zerodepth_indices_are_real_commas(self):
232+
def prop(s):
233+
idx = zeroDepthSearch(s, ",")
234+
return all(s[i] == u"," for i in idx) and idx == sorted(idx) and len(set(idx)) == len(idx)
235+
for_all(self, gen_text, prop, label="zerodepth-commas-text")
236+
for_all(self, gen_sql_fields, prop, label="zerodepth-commas-sql")
237+
238+
239+
class TestIdentifierRoundTrip(unittest.TestCase):
240+
def setUp(self):
241+
self._saved = kb.get("forcedDbms")
242+
set_dbms("MySQL") # identifier quoting is DBMS-specific; pin a case-preserving back-end
243+
244+
def tearDown(self):
245+
kb.forcedDbms = self._saved
246+
247+
def test_safe_unsafe_roundtrip(self):
248+
for_all(self, gen_ident, lambda n: unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming(n)) == n, label="identifier")
249+
250+
251+
class TestRobustness(unittest.TestCase):
252+
# total functions: must never raise on arbitrary text (return value unconstrained)
253+
def test_urlencode_urldecode(self):
254+
for_all(self, gen_text, lambda s: (urlencode(s), urldecode(s)) and True, label="urlcodec")
255+
256+
def test_safecharencode(self):
257+
for_all(self, gen_text, lambda s: safecharencode(s) is not None or s == u"", label="safecharencode")
258+
259+
def test_stdoutencode(self):
260+
for_all(self, gen_text, lambda s: stdoutEncode(s) is not None or s == u"", label="stdoutencode")
261+
262+
263+
if __name__ == "__main__":
264+
unittest.main()

0 commit comments

Comments
 (0)