Switching from pickle to json for serialization

This commit is contained in:
Miroslav Štampar 2026-07-08 12:13:49 +02:00
parent e2c55c1ddc
commit 4504edaeb1
12 changed files with 933 additions and 273 deletions

View file

@ -5,11 +5,6 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
try:
import cPickle as pickle
except:
import pickle
import itertools
import os
import shutil
@ -86,7 +81,7 @@ class BigArray(list):
>>> _[0] = [None]
>>> _.index(0)
20
>>> import pickle; __ = pickle.loads(pickle.dumps(_))
>>> import copy; __ = copy.deepcopy(_)
>>> __.append(1)
>>> len(_)
100001
@ -158,9 +153,10 @@ class BigArray(list):
self.chunks[-1] = self.cache.data
self.cache.dirty = False
else:
from lib.core.convert import deserializeValue
try:
with open(filename, "rb") as f:
self.chunks[-1] = pickle.loads(zlib.decompress(f.read()))
self.chunks[-1] = deserializeValue(zlib.decompress(f.read()))
except IOError as ex:
errMsg = "exception occurred while retrieving data "
errMsg += "from a temporary file ('%s')" % ex
@ -207,11 +203,13 @@ class BigArray(list):
self.close()
def _dump(self, chunk):
from lib.core.convert import getBytes, serializeValue
try:
handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.BIG_ARRAY)
self.filenames.add(filename)
with os.fdopen(handle, "w+b") as f:
f.write(zlib.compress(pickle.dumps(chunk, pickle.HIGHEST_PROTOCOL), BIGARRAY_COMPRESS_LEVEL))
# serializeValue() returns text; encode to bytes for the compressed on-disk chunk
f.write(zlib.compress(getBytes(serializeValue(chunk)), BIGARRAY_COMPRESS_LEVEL))
return filename
except (OSError, IOError) as ex:
errMsg = "exception occurred while storing data "
@ -240,9 +238,10 @@ class BigArray(list):
pass
if not (self.cache and self.cache.index == index):
from lib.core.convert import deserializeValue
try:
with open(self.chunks[index], "rb") as f:
self.cache = Cache(index, pickle.loads(zlib.decompress(f.read())), False)
self.cache = Cache(index, deserializeValue(zlib.decompress(f.read())), False)
except Exception as ex:
errMsg = "exception occurred while retrieving data "
errMsg += "from a temporary file ('%s')" % ex
@ -361,8 +360,9 @@ class BigArray(list):
if cache_index == idx and cache_data is not None:
data = cache_data
else:
from lib.core.convert import deserializeValue
with open(chunk, "rb") as f:
data = pickle.loads(zlib.decompress(f.read()))
data = deserializeValue(zlib.decompress(f.read()))
except Exception as ex:
errMsg = "exception occurred while retrieving data "
errMsg += "from a temporary file ('%s')" % ex

View file

@ -53,14 +53,14 @@ from lib.core.compat import LooseVersion
from lib.core.compat import RecursionError
from lib.core.compat import round
from lib.core.compat import xrange
from lib.core.convert import base64pickle
from lib.core.convert import base64unpickle
from lib.core.convert import decodeBase64
from lib.core.convert import deserializeValue
from lib.core.convert import decodeHex
from lib.core.convert import getBytes
from lib.core.convert import getText
from lib.core.convert import getUnicode
from lib.core.convert import htmlUnescape
from lib.core.convert import serializeValue
from lib.core.convert import stdoutEncode
from lib.core.data import cmdLineOptions
from lib.core.data import conf
@ -5071,7 +5071,7 @@ def serializeObject(object_):
True
"""
return base64pickle(object_)
return serializeValue(object_)
def unserializeObject(value):
"""
@ -5079,11 +5079,11 @@ def unserializeObject(value):
>>> unserializeObject(serializeObject([1, 2, 3])) == [1, 2, 3]
True
>>> unserializeObject('gAJVBmZvb2JhcnEBLg==')
'foobar'
>>> unserializeObject(serializeObject('foobar')) == 'foobar'
True
"""
return base64unpickle(value) if value else None
return deserializeValue(value) if value else None
def resetCounter(technique):
"""

View file

@ -5,14 +5,11 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
try:
import cPickle as pickle
except:
import pickle
import base64
import binascii
import codecs
import datetime
import decimal
import json
import re
import sys
@ -26,7 +23,6 @@ from lib.core.settings import INVALID_UNICODE_PRIVATE_AREA
from lib.core.settings import IS_TTY
from lib.core.settings import IS_WIN
from lib.core.settings import NULL
from lib.core.settings import PICKLE_PROTOCOL
from lib.core.settings import SAFE_HEX_MARKER
from lib.core.settings import UNICODE_ENCODING
from thirdparty import six
@ -41,48 +37,228 @@ except ImportError:
htmlEscape = _escape
def base64pickle(value):
"""
Serializes (with pickle) and encodes to Base64 format supplied (binary) value
# Safe (no arbitrary code execution) serialization used for the session store (HashDB)
# and BigArray disk chunks. The former serializer could execute code while loading, so
# deserializing sqlmap's own (locally writable) session/cache files was a recurring
# report magnet. This codec serializes to plain JSON with explicit type tags, so nothing
# is ever executed on load.
#
# JSON natively covers only str/int/float/bool/None/list, and silently loses the rest
# (int/tuple dict keys become strings, set/tuple/bytes are rejected). The tagged wrappers
# below preserve every type sqlmap actually stores: bytes, tuple, set/frozenset, dict with
# arbitrary (non-string) keys, DB-driver scalars (Decimal/datetime/...), and the handful of
# sqlmap's own classes below. Reconstruction of classes is limited to that explicit
# allowlist (no module/namespace wildcard), so no dangerous callable is ever reachable.
>>> base64unpickle(base64pickle([1, 2, 3])) == [1, 2, 3]
True
>>> isinstance(base64unpickle(base64pickle(BigArray([1, 2, 3]))), BigArray)
True
# reserved wrapper key; data mappings are encoded as tagged pair-lists (never as bare JSON
# objects), so any decoded JSON object is one of our wrappers and this key can never collide
_SERIALIZE_TAG = "$T"
# fully-qualified names of the ONLY classes that may be reconstructed on deserialization
_SERIALIZE_CLASSES = frozenset((
"lib.core.datatype.AttribDict",
"lib.core.datatype.InjectionDict",
"lib.utils.har.RawPair",
))
def _serializeEncode(value):
"""
Turns a Python value into a JSON-serializable (tagged) structure
"""
retVal = None
if value is None or isinstance(value, bool) or isinstance(value, float) or isinstance(value, six.integer_types):
return value
if isinstance(value, six.text_type):
return value
# Note: on Python 2 'str' is binary; base64-tagging it (rather than emitting a native JSON
# string that would round-trip as 'unicode') keeps the exact byte type across versions
if isinstance(value, (six.binary_type, bytearray)):
raw = bytes(value) if isinstance(value, bytearray) else value
return {_SERIALIZE_TAG: "b", "v": encodeBase64(raw, binary=False), "a": 1 if isinstance(value, bytearray) else 0}
if isinstance(value, memoryview):
return {_SERIALIZE_TAG: "b", "v": encodeBase64(value.tobytes(), binary=False), "a": 0}
try:
retVal = encodeBase64(pickle.dumps(value, PICKLE_PROTOCOL), binary=False)
except:
warnMsg = "problem occurred while serializing "
warnMsg += "instance of a type '%s'" % type(value)
singleTimeWarnMessage(warnMsg)
if isinstance(value, buffer): # noqa: F821 # Python 2 only
return {_SERIALIZE_TAG: "b", "v": encodeBase64(bytes(value), binary=False), "a": 0}
except NameError:
pass
try:
retVal = encodeBase64(pickle.dumps(value), binary=False)
except:
raise
# Note: BigArray is a 'list' subclass, so it must be matched before the plain-list branch
# (otherwise it would round-trip as a plain list, losing its type)
if isinstance(value, BigArray):
return {_SERIALIZE_TAG: "ba", "v": [_serializeEncode(_) for _ in value]}
if isinstance(value, list):
return [_serializeEncode(_) for _ in value]
if isinstance(value, tuple):
return {_SERIALIZE_TAG: "t", "v": [_serializeEncode(_) for _ in value]}
if isinstance(value, frozenset):
return {_SERIALIZE_TAG: "f", "v": [_serializeEncode(_) for _ in value]}
if isinstance(value, (set, _collections.Set)):
return {_SERIALIZE_TAG: "s", "v": [_serializeEncode(_) for _ in value]}
if isinstance(value, dict):
name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__)
if name in _SERIALIZE_CLASSES:
return {_SERIALIZE_TAG: "o", "c": name, "d": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()], "s": _serializeEncode(dict(value.__dict__))}
elif value.__class__ is dict:
return {_SERIALIZE_TAG: "m", "v": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()]}
else:
return _serializeUnknown(value, name)
if isinstance(value, decimal.Decimal):
return {_SERIALIZE_TAG: "dec", "v": getUnicode(value)}
if isinstance(value, datetime.datetime):
return {_SERIALIZE_TAG: "dt", "v": [value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond]}
if isinstance(value, datetime.date):
return {_SERIALIZE_TAG: "date", "v": [value.year, value.month, value.day]}
if isinstance(value, datetime.time):
return {_SERIALIZE_TAG: "time", "v": [value.hour, value.minute, value.second, value.microsecond]}
if isinstance(value, datetime.timedelta):
return {_SERIALIZE_TAG: "td", "v": [value.days, value.seconds, value.microseconds]}
name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__)
if name in _SERIALIZE_CLASSES:
return {_SERIALIZE_TAG: "o", "c": name, "s": _serializeEncode(dict(value.__dict__))}
return _serializeUnknown(value, name)
def _serializeUnknown(value, name):
"""
Fallback for a type not explicitly handled by the serializer
"""
# sqlmap's own (or bundled) classes MUST be added to the allowlist explicitly - fail loudly
# (caught by the regression tests) rather than silently store something that cannot be restored
if (name or "").split(".")[0] in ("lib", "plugins", "thirdparty"):
raise TypeError("serialization of type '%s' is not supported" % name)
# a foreign/exotic scalar (e.g. an unusual DB-driver value): degrade to its textual form rather
# than crash a user's session - session values are only ever rendered (getUnicode) downstream
singleTimeWarnMessage("serializing value of unsupported type '%s' as text" % name)
return getUnicode(value)
def _serializeDecode(struct):
"""
Restores a Python value from a JSON-deserialized (tagged) structure
"""
if struct is None or isinstance(struct, bool) or isinstance(struct, float) or isinstance(struct, six.integer_types):
return struct
if isinstance(struct, six.text_type):
return struct
if isinstance(struct, six.binary_type): # defensive - json.loads() yields text, not bytes
return getUnicode(struct)
if isinstance(struct, list):
return [_serializeDecode(_) for _ in struct]
if isinstance(struct, dict):
tag = struct.get(_SERIALIZE_TAG)
if tag == "b":
raw = decodeBase64(struct["v"], binary=True)
return bytearray(raw) if struct.get("a") else raw
elif tag == "t":
return tuple(_serializeDecode(_) for _ in struct["v"])
elif tag == "f":
return frozenset(_serializeDecode(_) for _ in struct["v"])
elif tag == "ba":
return BigArray([_serializeDecode(_) for _ in struct["v"]])
elif tag == "s":
return set(_serializeDecode(_) for _ in struct["v"])
elif tag == "m":
return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct["v"])
elif tag == "dec":
return decimal.Decimal(struct["v"])
elif tag == "dt":
return datetime.datetime(*struct["v"])
elif tag == "date":
return datetime.date(*struct["v"])
elif tag == "time":
return datetime.time(*struct["v"])
elif tag == "td":
return datetime.timedelta(struct["v"][0], struct["v"][1], struct["v"][2])
elif tag == "o":
return _serializeDecodeObject(struct)
elif tag is None: # defensive - a bare mapping should never occur
return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct.items())
else:
raise ValueError("unsupported serialized tag '%s'" % tag)
raise ValueError("unsupported serialized structure of type '%s'" % type(struct))
def _serializeResolveClass(name):
"""
Resolves an allowlisted class name to its class (nothing else may be reconstructed)
"""
if name not in _SERIALIZE_CLASSES:
raise ValueError("deserialization of class '%s' is forbidden" % name)
if name == "lib.utils.har.RawPair":
from lib.utils.har import RawPair
return RawPair
else:
from lib.core.datatype import AttribDict, InjectionDict
return InjectionDict if name.endswith("InjectionDict") else AttribDict
def _serializeDecodeObject(struct):
"""
Reconstructs an allowlisted class instance from its serialized form
"""
_class = _serializeResolveClass(struct.get("c"))
retVal = _class.__new__(_class)
if isinstance(retVal, dict):
for pair in (struct.get("d") or []):
dict.__setitem__(retVal, _serializeDecode(pair[0]), _serializeDecode(pair[1]))
state = _serializeDecode(struct.get("s") or {})
if isinstance(state, dict):
retVal.__dict__.update(state)
return retVal
def base64unpickle(value):
def serializeValue(value):
"""
Decodes value from Base64 to plain format and deserializes (with pickle) its content
Safely serializes a Python value to its canonical serialized form (JSON text), without any
code-execution risk
>>> type(base64unpickle('gAJjX19idWlsdGluX18Kb2JqZWN0CnEBKYFxAi4=')) == object
Note: the output is pure ASCII text, so it is stored verbatim in the (TEXT) session store - no
Base64 (or any base-N) wrapping is needed (that was only required by the former binary
serialization), which also keeps the stored form as small as possible. Callers that need raw
bytes (e.g. a compressed BigArray disk chunk) simply encode the returned text.
>>> deserializeValue(serializeValue({1: 'a', 'b': (2, 3), 'c': {4, 5}})) == {1: 'a', 'b': (2, 3), 'c': {4, 5}}
True
>>> deserializeValue(serializeValue([1, 2, (3, {4: b'5'})])) == [1, 2, (3, {4: b'5'})]
True
"""
retVal = None
return json.dumps(_serializeEncode(value), ensure_ascii=True, separators=(',', ':'))
try:
retVal = pickle.loads(decodeBase64(value))
except TypeError:
retVal = pickle.loads(decodeBase64(bytes(value)))
def deserializeValue(value):
"""
Restores a Python value from its serialized form (accepts the serialized data as either text or
bytes)
"""
return retVal
return _serializeDecode(json.loads(getText(value)))
def htmlUnescape(value):
"""

View file

@ -178,52 +178,6 @@ def dirtyPatches():
et.parse = _safe_parse
et._patched = True
import io
import pickle
if not getattr(pickle, "_patched", False):
class RestrictedUnpickler(pickle.Unpickler):
# Note: allowlist (not blacklist) - a module blacklist is bypassable (e.g. importlib/ctypes/operator), so only
# explicitly-safe builtin data types and sqlmap's own (and bundled) classes are permitted to be unpickled
def find_class(self, module, name):
# Note: protocol-2 pickling of a 'bytes' value on Python 3 emits a _codecs.encode global; allow that one
# (it only runs a codec, e.g. latin1 - it cannot execute arbitrary code) so serialized values containing
# bytes round-trip. Everything else from _codecs (e.g. lookup) stays blocked by the rule below.
if module == "_codecs" and name == "encode":
pass
# safe builtin data types only (blocks eval/exec/__import__/getattr/etc.)
elif module in ("builtins", "__builtin__"):
if name not in ("set", "frozenset", "dict", "list", "tuple", "int", "float", "bool", "str", "bytes", "bytearray", "object", "NoneType", "complex"):
raise ValueError("unpickling of '%s.%s' is forbidden" % (module, name))
# everything else must be one of sqlmap's own (or bundled) classes (e.g. lib.core.datatype.AttribDict)
elif (module or "").split(".")[0] not in ("lib", "plugins", "thirdparty"):
raise ValueError("unpickling of module '%s' is forbidden" % module)
# Python 2/3 method resolution
if hasattr(pickle.Unpickler, "find_class"):
return pickle.Unpickler.find_class(self, module, name)
__import__(module)
return getattr(sys.modules[module], name)
def _safe_loads(data):
try:
stream = io.BytesIO(data)
except TypeError:
stream = io.StringIO(data)
return RestrictedUnpickler(stream).load()
pickle.loads = _safe_loads
pickle._patched = True
try:
import cPickle
if not getattr(cPickle, "_patched", False):
cPickle.loads = pickle.loads
cPickle._patched = True
except ImportError:
pass
try:
import builtins
except ImportError:

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.39"
VERSION = "1.10.7.40"
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)
@ -868,11 +868,8 @@ HASHDB_RETRIEVE_RETRIES = 3
# Number of retries for unsuccessful HashDB end transaction attempts
HASHDB_END_TRANSACTION_RETRIES = 3
# Unique milestone value used for forced deprecation of old HashDB values (e.g. when changing hash/pickle mechanism)
HASHDB_MILESTONE_VALUE = "GpqxbkWTfz" # python -c 'import random, string; print "".join(random.sample(string.ascii_letters, 10))'
# Pickle protocl used for storage of serialized data inside HashDB (https://docs.python.org/3/library/pickle.html#data-stream-format)
PICKLE_PROTOCOL = 2
# Unique milestone value used for forced deprecation of old HashDB values (e.g. when changing the hash/serialization mechanism)
HASHDB_MILESTONE_VALUE = "MvKpZrBqTn" # python -c 'import random, string; print "".join(random.sample(string.ascii_letters, 10))'
# Warn user of possible delay due to large page dump in full UNION query injections
LARGE_OUTPUT_THRESHOLD = 1024 ** 2

View file

@ -6,10 +6,11 @@ See the file 'LICENSE' for copying permission
"""
from lib.core.common import Backend
from lib.core.datatype import AttribDict
from lib.core.settings import EXCLUDE_UNESCAPE
class Unescaper(AttribDict):
# Note: a plain dict (DBMS -> escape function) with a helper method; it is a runtime registry, never
# serialized, so it deliberately does NOT use AttribDict (no attribute-style access is needed here)
class Unescaper(dict):
def escape(self, expression, quote=True, dbms=None):
if expression is None:
return expression

View file

@ -165,8 +165,9 @@ class HashDB(object):
self._write_cache = {}
self._last_flush_time = time.time()
began = False
try:
self.beginTransaction()
began = self.beginTransaction()
for hash_, value in flush_cache.items():
retries = 0
while True:
@ -197,23 +198,30 @@ class HashDB(object):
else:
break
finally:
self.endTransaction()
# Only close a transaction we actually opened; when flush() runs nested inside an
# outer batch (e.g. lib/utils/hash.py wrapping cracked-password writes) beginTransaction()
# returns False and the outer owner keeps ownership - ending it here would commit it early
if began:
self.endTransaction()
def beginTransaction(self):
threadData = getCurrentThreadData()
if not threadData.inTransaction:
if threadData.inTransaction:
return False # already inside an (outer) transaction; do not nest
try:
self.cursor.execute("BEGIN TRANSACTION")
except Exception: # Note: deliberately not bare - a KeyboardInterrupt here must propagate
try:
self.cursor.execute("BEGIN TRANSACTION")
except:
try:
# Reference: http://stackoverflow.com/a/25245731
self.cursor.close()
except sqlite3.ProgrammingError:
pass
threadData.hashDBCursor = None
self.cursor.execute("BEGIN TRANSACTION")
finally:
threadData.inTransaction = True
# Reference: http://stackoverflow.com/a/25245731
self.cursor.close()
except sqlite3.ProgrammingError:
pass
threadData.hashDBCursor = None
self.cursor.execute("BEGIN TRANSACTION")
threadData.inTransaction = True # set only on a genuine BEGIN (not if the retry above raised)
return True
def endTransaction(self):
threadData = getCurrentThreadData()

View file

@ -21,7 +21,7 @@ bootstrap()
from lib.core.convert import (decodeHex, encodeHex, decodeBase64, encodeBase64,
getBytes, getText, getUnicode, getOrds,
jsonize, dejsonize, base64pickle, base64unpickle)
jsonize, dejsonize, serializeValue, deserializeValue)
from lib.core.common import decodeDbmsHexValue
try:
@ -109,36 +109,31 @@ class TestJson(unittest.TestCase):
self.assertEqual(jsonize(123), "123") # int -> textual "123"
class TestBase64Pickle(unittest.TestCase):
# Types sqlmap actually serializes (injection objects, cached values, BigArray).
class TestSerialize(unittest.TestCase):
# Smoke coverage of the safe (JSON-based) session serializer; the exhaustive corner-case
# and security suite lives in tests/test_serialize.py.
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)
self.assertEqual(deserializeValue(serializeValue(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)
self.assertEqual(deserializeValue(serializeValue(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,))
self.assertEqual(deserializeValue(serializeValue(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))
def test_output_is_plain_ascii_text_no_base64(self):
# the serialized form must be readable JSON text (not Base64 / binary) so it lands verbatim in
# the TEXT session column with zero wrapping overhead
out = serializeValue({"k": [1, (2, 3)]})
self.assertIsInstance(out, str)
self.assertTrue(out.startswith("{") and out.endswith("}"), out)
def test_deserialize_accepts_bytes(self):
# BigArray hands the serialized data back as bytes (off a compressed disk chunk)
self.assertEqual(deserializeValue(getBytes(serializeValue([1, (2, 3)]))), [1, (2, 3)])
if __name__ == "__main__":

462
tests/test_serialize.py Normal file
View file

@ -0,0 +1,462 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Exhaustive lock on the safe (JSON-based, no code execution) serializer that backs the
session store (HashDB) and BigArray disk chunks - the replacement for the former
code-executing serializer.
Two properties must hold forever, on BOTH Python 2.7 and 3.x:
1. CORRECTNESS - every value sqlmap actually persists must round-trip losslessly, with
its exact type. The historically fragile cases are covered explicitly: integer/tuple
dict keys (naive JSON turns them into strings), tuple-vs-list, set/frozenset, bytes,
the AttribDict/InjectionDict/RawPair classes and their internal state, and the native
DB-driver scalars (Decimal/datetime/...) that '-d' direct-mode output can contain.
A regression here silently corrupts a user's saved session.
2. SECURITY - deserialization must NEVER execute code and must reconstruct ONLY the small
explicit class allowlist. Any other class name (os.system, subprocess.Popen, eval,
lib.core.common.shellExec, ...) must be refused with a "forbidden" ValueError, and a
crafted legacy payload must fail inertly (no side effects).
Kept deliberately verbose - each case maps to a real session/BigArray value or a real report.
"""
import datetime
import decimal
import json
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.bigarray import BigArray
from lib.core.convert import deserializeValue, encodeBase64, getUnicode, serializeValue
from lib.core.convert import _SERIALIZE_TAG
from lib.core.datatype import AttribDict, InjectionDict, LRUDict
from lib.utils.har import RawPair
from thirdparty import six
INTS = six.integer_types
_unichr = six.unichr
def rt(value):
"""Full session round-trip: value -> JSON text -> value."""
return deserializeValue(serializeValue(value))
def _import_all_modules():
"""Import every sqlmap module (like --smoke-test) so class-subclass reflection sees them all."""
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
for base in ("lib", "plugins", "tamper"):
for dirpath, _, filenames in os.walk(os.path.join(root, base)):
if any(skip in dirpath for skip in ("thirdparty", "extra", "tests", "__pycache__")):
continue
for filename in filenames:
if filename.endswith(".py") and filename not in ("__init__.py", "gui.py"):
dotted = os.path.join(dirpath, filename[:-3]).replace(root + os.sep, "").replace(os.sep, ".")
try:
__import__(dotted)
except Exception:
pass # import health is --smoke-test's job; here we only need the classes that DO load
def _all_subclasses(cls):
for sub in cls.__subclasses__():
yield sub
for _ in _all_subclasses(sub):
yield _
class TestScalars(unittest.TestCase):
def test_simple(self):
for value in [None, True, False, 0, 1, -1, 42, 3.14, -0.5, 0.0]:
restored = rt(value)
self.assertEqual(restored, value)
self.assertIs(type(restored) is bool, type(value) is bool) # bool never collapses to int
def test_big_ints(self):
for value in [2 ** 64, 2 ** 200, -(2 ** 128)]:
self.assertEqual(rt(value), value)
def test_text(self):
# empty, ascii, non-BMP, sqlmap's reversible-codec private-use-area char, control chars
for value in [u"", u"plain", _unichr(0x2299) + u" mid " + _unichr(0xFF),
_unichr(0x1F600) if sys.maxunicode > 0xFFFF else u"x",
_unichr(0xF0055), u"tab\tnewline\nquote\"backslash\\"]:
restored = rt(value)
self.assertEqual(restored, value)
self.assertIsInstance(restored, six.text_type)
class TestBinary(unittest.TestCase):
def test_bytes(self):
for value in [b"", b"abc", b"\x00\x01\x02\xff\xfe", bytes(bytearray(range(256)))]:
restored = rt(value)
self.assertEqual(restored, value)
self.assertIsInstance(restored, bytes)
def test_bytearray(self):
value = bytearray(b"\x00binary\xff")
restored = rt(value)
self.assertEqual(restored, value)
self.assertIsInstance(restored, bytearray)
def test_memoryview(self):
# some drivers (e.g. psycopg2 on py3) return memoryview for BLOB columns
restored = rt(memoryview(b"\x00\x01blob"))
self.assertEqual(bytes(restored) if isinstance(restored, (bytearray, memoryview)) else restored, b"\x00\x01blob")
class TestContainers(unittest.TestCase):
def test_list_nested(self):
value = [1, [2, [3, [4]]], "x", None]
self.assertEqual(rt(value), value)
def test_tuple_preserved(self):
for value in [(), (1,), (1, 2, "x"), ((1, 2), (3,))]:
restored = rt(value)
self.assertEqual(restored, value)
self.assertIsInstance(restored, tuple) # must NOT degrade to list
def test_tuple_not_confused_with_list(self):
restored = rt([(1, 2), [1, 2]])
self.assertIsInstance(restored[0], tuple)
self.assertIsInstance(restored[1], list)
def test_set_and_frozenset(self):
for value in [set(), {1, 2, 3}, {u"a", u"b"}]:
restored = rt(value)
self.assertEqual(restored, value)
self.assertIsInstance(restored, set)
fs = frozenset([1, 2])
restored = rt(fs)
self.assertEqual(restored, fs)
self.assertIsInstance(restored, frozenset)
class TestDicts(unittest.TestCase):
def test_string_keys(self):
value = {u"a": 1, u"b": {u"c": [1, 2]}}
self.assertEqual(rt(value), value)
def test_int_keys_preserved(self):
# THE landmine: naive json.dumps turns {1: ...} into {"1": ...}; injection.data is int-keyed
restored = rt({1: u"one", 2: u"two", 100: u"hundred"})
self.assertEqual(restored, {1: u"one", 2: u"two", 100: u"hundred"})
self.assertTrue(all(isinstance(k, INTS) for k in restored), list(restored))
def test_tuple_and_mixed_keys(self):
value = {(1, 2): u"tuple-key", u"s": 1, 7: u"int-key"}
restored = rt(value)
self.assertEqual(restored, value)
self.assertIn((1, 2), restored)
self.assertTrue(any(isinstance(k, INTS) and not isinstance(k, bool) for k in restored))
def test_empty_dict(self):
self.assertEqual(rt({}), {})
class TestSqlmapTypes(unittest.TestCase):
def test_attribdict(self):
value = AttribDict()
value.foo = u"bar"
value["n"] = {u"k": [1, (2, 3)]}
restored = rt(value)
self.assertIsInstance(restored, AttribDict)
self.assertEqual(restored.foo, u"bar")
self.assertEqual(restored["n"], {u"k": [1, (2, 3)]})
def test_attribdict_keycheck_flag_preserved(self):
strict = AttribDict(keycheck=True)
lax = AttribDict(keycheck=False)
self.assertTrue(rt(strict).__dict__.get("_keycheck"))
self.assertFalse(rt(lax).__dict__.get("_keycheck"))
# a lax AttribDict returns None for a missing attribute instead of raising - behaviour must survive
self.assertIsNone(rt(lax).nonexistent_attribute)
def test_injectiondict_full(self):
inj = InjectionDict()
inj.place = "GET"
inj.parameter = "id"
inj.ptype = 1
inj.prefix = ""
inj.suffix = ""
inj.dbms = "MySQL"
inj.notes = ["note1"]
# int-keyed .data (PAYLOAD.TECHNIQUE.* are ints), each a nested AttribDict
for stype in (1, 5):
data = AttribDict()
data.title = "technique %d" % stype
data.payload = u"id=1 AND %d=%d" % (stype, stype)
data.where = 1
data.vector = None
data.matchRatio = 0.987
data.trueCode = 200
data.falseCode = 500
inj.data[stype] = data
inj.conf = AttribDict()
inj.conf.textOnly = False
restored = rt([inj])[0]
self.assertIsInstance(restored, InjectionDict)
self.assertEqual(restored.place, "GET")
self.assertEqual(restored.parameter, "id")
self.assertEqual(restored.notes, ["note1"])
self.assertEqual(set(restored.data.keys()), set((1, 5)))
self.assertTrue(all(isinstance(k, INTS) for k in restored.data), list(restored.data))
self.assertIsInstance(restored.data[1], AttribDict)
self.assertEqual(restored.data[1].title, "technique 1")
self.assertEqual(restored.data[1].matchRatio, 0.987)
self.assertIsNone(restored.data[1].vector)
self.assertIsInstance(restored.conf, AttribDict)
self.assertFalse(restored.conf.textOnly)
def test_bigarray_type_preserved(self):
# BigArray is a list subclass; it must round-trip AS a BigArray, not degrade to a plain list
restored = rt(BigArray([1, 2, (3, 4), u"x"]))
self.assertIsInstance(restored, BigArray)
self.assertEqual(list(restored), [1, 2, (3, 4), u"x"])
def test_rawpair(self):
# the class stored in a BigArray by the HAR collector
pair = RawPair(b"GET / HTTP/1.1", b"HTTP/1.1 200 OK", startTime=1.5, endTime=2.5, extendedArguments={u"k": u"v"})
restored = rt(pair)
self.assertIsInstance(restored, RawPair)
self.assertEqual(restored.request, b"GET / HTTP/1.1")
self.assertEqual(restored.response, b"HTTP/1.1 200 OK")
self.assertEqual(restored.startTime, 1.5)
self.assertEqual(restored.extendedArguments, {u"k": u"v"})
class TestDbmsScalars(unittest.TestCase):
# '-d' direct-mode query output (and dump BigArray) can hold native driver types
def test_decimal(self):
for value in [decimal.Decimal("0"), decimal.Decimal("1.50"), decimal.Decimal("-0.0001"), decimal.Decimal("123456789.987654321")]:
restored = rt(value)
self.assertEqual(restored, value)
self.assertIsInstance(restored, decimal.Decimal)
def test_datetime_family(self):
cases = [
datetime.datetime(2024, 1, 2, 3, 4, 5, 6),
datetime.datetime(1970, 1, 1, 0, 0, 0),
datetime.date(1999, 12, 31),
datetime.time(23, 59, 58, 123456),
datetime.timedelta(days=3, seconds=7, microseconds=9),
datetime.timedelta(0),
]
for value in cases:
restored = rt(value)
self.assertEqual(restored, value)
self.assertIs(type(restored), type(value))
class TestRealSessionObjects(unittest.TestCase):
# the exact shapes written with serialize=True across the codebase
def test_brute_tables_columns(self):
tables = [("mydb", "users"), ("mydb", "logs")]
columns = [("mydb", "users", "id", "numeric"), ("mydb", "users", "name", "non-numeric")]
self.assertEqual(rt(tables), tables)
self.assertEqual(rt(columns), columns)
self.assertTrue(all(isinstance(_, tuple) for _ in rt(tables)))
def test_dynamic_markings(self):
markings = [(u"<b>", None), (None, u"</div>"), (u"pre", u"suf")]
self.assertEqual(rt(markings), markings)
self.assertTrue(all(isinstance(_, tuple) for _ in rt(markings)))
def test_abs_file_paths_set(self):
paths = set([u"/var/www/index.php", u"/etc/passwd"])
restored = rt(paths)
self.assertEqual(restored, paths)
self.assertIsInstance(restored, set) # resume code does an explicit isinstance(_, set) union
def test_kb_chars_attribdict(self):
chars = AttribDict()
chars.delimiter = u"abcdef"
chars.start = u"//start//"
chars.stop = u"//stop//"
restored = rt(chars)
self.assertIsInstance(restored, AttribDict)
self.assertEqual(restored.delimiter, u"abcdef")
class TestSecurity(unittest.TestCase):
def _forged(self, class_name):
# the raw on-wire object wrapper as a hostile session file would hold it (a plain JSON
# object tag; NOT produced via the encoder, which would re-tag a dict as a harmless map)
return json.dumps({_SERIALIZE_TAG: "o", "c": class_name, "s": {}})
def test_forbidden_classes_rejected(self):
for name in ("os.system", "subprocess.Popen", "builtins.eval", "__builtin__.eval",
"lib.core.common.shellExec", "lib.core.common.evaluateCode", "lib.core.common.openFile"):
try:
deserializeValue(self._forged(name))
self.fail("class %r was NOT rejected" % name)
except ValueError as ex:
self.assertIn("forbidden", str(ex), msg="unexpected error for %r: %s" % (name, ex))
def test_legacy_payload_fails_inertly(self):
# an OLD session stored base64-of-pickle as TEXT; the new text reader must fail on it
# WITHOUT executing anything (and, in practice, the bumped HASHDB_MILESTONE_VALUE means such
# a value is never even looked up). Use a classic protocol-0 os.system pickle as the payload.
sentinel = os.path.join(tempfile.gettempdir(), "sqlmap_serialize_sentinel_%d" % os.getpid())
if os.path.exists(sentinel):
os.remove(sentinel)
legacy_pickle = b"cos\nsystem\n(S'touch " + sentinel.encode("ascii", "ignore") + b"'\ntR."
legacy_stored = encodeBase64(legacy_pickle, binary=False) # exactly what old sqlmap wrote
try:
deserializeValue(legacy_stored) # base64 blob is not valid JSON -> raises
except Exception:
pass # any failure is fine; the point is no execution
self.assertFalse(os.path.exists(sentinel), "legacy payload EXECUTED (sentinel created)")
def test_hashdb_ignores_undecodable_old_value(self):
# the belt-and-suspenders guarantee: even if an un-deserializable (e.g. legacy) value IS
# looked up, HashDB.retrieve() swallows it and returns None - a stale session never crashes
from lib.utils.hashdb import HashDB
handle, path = tempfile.mkstemp(suffix=".sqlite")
os.close(handle)
os.remove(path)
try:
db = HashDB(path)
db.write("legacy", encodeBase64(b"cos\nsystem\n(S'x'\ntR.", binary=False)) # stored, NOT serialized
db.flush()
db._write_cache.clear()
db._read_cache.cache.clear()
self.assertIsNone(db.retrieve("legacy", unserialize=True)) # must be None, not an exception
db.closeAll()
finally:
if os.path.exists(path):
os.remove(path)
def test_sqlmap_type_not_in_allowlist_fails_loud(self):
# a sqlmap-own class that is not allowlisted must raise on SERIALIZE (dev-visible), never be
# silently dropped - LRUDict lives in lib.* and is not serializable
self.assertRaises(TypeError, serializeValue, LRUDict(capacity=2))
def test_foreign_exotic_value_degrades_to_text(self):
# a non-sqlmap, non-handled scalar degrades to its textual form rather than crashing a session
self.assertEqual(rt(complex(1, 2)), getUnicode(complex(1, 2)))
class TestBigArrayDisk(unittest.TestCase):
def test_scalar_chunks_through_disk(self):
ba = BigArray(chunk_size=1) # force one chunk per item -> exercises the on-disk path
source = []
for i in range(300):
row = (i, u"name%d" % i, decimal.Decimal("%d.25" % i), None, b"\x00\xff")
source.append(row)
ba.append(row)
self.assertGreater(len(ba.chunks), 1)
self.assertEqual(len(ba), 300)
self.assertEqual(list(ba), source)
self.assertEqual(ba[150], source[150])
self.assertEqual(ba[-1], source[-1])
def test_rawpair_chunks_through_disk(self):
ba = BigArray(chunk_size=1)
for i in range(40):
ba.append(RawPair(b"REQ%d" % i, b"RESP%d" % i, startTime=float(i), endTime=float(i) + 1, extendedArguments={u"i": i}))
got = list(ba)
self.assertEqual(len(got), 40)
self.assertTrue(all(isinstance(_, RawPair) for _ in got))
self.assertEqual(got[7].request, b"REQ7")
self.assertEqual(got[7].extendedArguments, {u"i": 7})
class TestAllowlistGuard(unittest.TestCase):
"""CI guard: catch a NEW class becoming serializable before it reaches a user's session."""
def test_allowlist_names_resolve_and_stay_in_sync(self):
# every allowlisted name must resolve to a class whose real module.qualname matches - keeps
# convert._SERIALIZE_CLASSES and convert._serializeResolveClass in lockstep (a rename/move/
# typo of an allowlisted class fails here instead of silently at a user's session load)
from lib.core.convert import _SERIALIZE_CLASSES, _serializeResolveClass
for name in _SERIALIZE_CLASSES:
cls = _serializeResolveClass(name)
self.assertEqual("%s.%s" % (cls.__module__, cls.__name__), name)
def test_no_undeclared_attribdict_subclass(self):
# AttribDict/InjectionDict carry the session's structured data. A NEW AttribDict subclass that
# ends up stored in the session would be REJECTED by the serializer at a user's runtime. Catch
# it here coverage-INDEPENDENTLY: import the whole tree, then require every AttribDict subclass
# to be declared serializable in convert._SERIALIZE_CLASSES.
from lib.core.convert import _SERIALIZE_CLASSES
from lib.core.datatype import AttribDict
_import_all_modules()
offenders = sorted(set(
"%s.%s" % (cls.__module__, cls.__name__)
for cls in _all_subclasses(AttribDict)
) - set(_SERIALIZE_CLASSES))
self.assertEqual(offenders, [], (
"undeclared AttribDict subclass(es): %s -- if stored in the session, add it to BOTH "
"convert._SERIALIZE_CLASSES and convert._serializeResolveClass (else the safe serializer "
"raises on it at a user's runtime). If it is a runtime-only registry, make it subclass a "
"plain dict instead of AttribDict (see lib.core.unescaper.Unescaper) so it never lands "
"here." % offenders
))
def test_known_serializable_classes_present(self):
# lock the intended set: exactly the dict-like data types plus the HAR RawPair. If this list
# legitimately grows, update it here deliberately (a conscious review point).
from lib.core.convert import _SERIALIZE_CLASSES
self.assertEqual(set(_SERIALIZE_CLASSES), set((
"lib.core.datatype.AttribDict",
"lib.core.datatype.InjectionDict",
"lib.utils.har.RawPair",
)))
class TestHashDBIntegration(unittest.TestCase):
def test_write_retrieve_roundtrip(self):
from lib.utils.hashdb import HashDB
handle, path = tempfile.mkstemp(suffix=".sqlite")
os.close(handle)
os.remove(path)
try:
inj = InjectionDict()
inj.place = "GET"
inj.parameter = "id"
inj.data[1] = AttribDict()
inj.data[1].title = "boolean-based blind"
inj.data[1].matchRatio = 0.9
value = [inj]
db = HashDB(path)
db.write("KB_INJECTIONS", value, serialize=True)
db.flush()
# drop the in-memory caches so retrieve() must SELECT the serialized blob back off
# disk and deserialize it (the real resume path), rather than returning a cached object
db._write_cache.clear()
db._read_cache.cache.clear()
restored = db.retrieve("KB_INJECTIONS", unserialize=True)
db.closeAll()
self.assertIsInstance(restored, list)
self.assertIsInstance(restored[0], InjectionDict)
self.assertEqual(restored[0].place, "GET")
self.assertTrue(all(isinstance(k, INTS) for k in restored[0].data))
self.assertEqual(restored[0].data[1].title, "boolean-based blind")
self.assertEqual(restored[0].data[1].matchRatio, 0.9)
finally:
if os.path.exists(path):
os.remove(path)
if __name__ == "__main__":
unittest.main(verbosity=2)

191
tests/test_sqllint.py Normal file
View file

@ -0,0 +1,191 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit tests + atom-level SQL coverage for lib/utils/sqllint.py.
Two concerns:
1. Linter self-test - a curated set of well-formed fragments/statements that
must NOT flag, malformed ones that MUST flag, and the cross-dialect edge
cases the linter has learned (==, ::, $/backtick identifiers, LIKE() as a
function, HSQLDB "LIMIT off lim", ...). Guards the linter from regressions.
2. Atom-level SQL coverage - every SQL-bearing template in data/xml/queries.xml
and data/xml/payloads.xml, across ALL back-end DBMSes, must lint structurally
clean. This is a coverage gate over the *building blocks* of every query
sqlmap emits. The composed/runtime layer (agent.py wrapping these atoms into
wire payloads) is exercised faithfully by the separate payload-lint walk; a
0-flag result here means the catalog itself is sound in all 30 dialects.
A regression (a newly malformed catalog entry, OR a genuinely new valid dialect
construct the linter does not yet understand) makes the relevant test fail with a
pointer to the offending template.
stdlib unittest only; Python 2.7 and 3.x; pure-ASCII.
"""
import os
import re
import sys
import unittest
import xml.etree.ElementTree as ET
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from lib.utils.sqllint import checkSanity
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# well-formed fragments/statements that must NOT flag
GOOD = (
"1 AND 1=1",
"1) AND 5108=5108 AND (7936=7936",
"1)) OR 1=1-- -",
"1' AND '1'='1",
"1 UNION ALL SELECT NULL,NULL,CONCAT(0x71,0x62,0x71)-- -",
"1 AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT(0x7176,(SELECT database()),0x71)x FROM information_schema.tables GROUP BY x)a)",
"1 AND ORD(MID((SELECT IFNULL(CAST(username AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),1,1))>64",
"1 AND EXTRACTVALUE(1,CONCAT(0x5c,0x7e,(SELECT version())))",
"SELECT name FROM users WHERE id=1 ORDER BY id",
"SELECT count(*) FROM t WHERE x=1",
"1 WAITFOR DELAY '0:0:5'",
)
# cross-dialect valid constructs the linter must accept (regression guards for
# the exact false positives that adversarial/catalog runs exposed and fixed)
DIALECT_GOOD = (
"1 AND id==1", # SQLite '==' equality
"SELECT 1 WHERE (SELECT 1)::text = '1'", # PostgreSQL '::' cast
"SELECT 1 WHERE 9223=LIKE(CHAR(65),UPPER(HEX(1)))", # SQLite LIKE() function
"SELECT NAME FROM SYSMASTER:SYSDATABASES", # Informix 'db:table'
"SELECT $ZVERSION", # InterSystems Cache system var
"SELECT `col` FROM `directory` LIMIT 0,1", # MySQL backtick identifiers
"SELECT LIMIT 0 1 DISTINCT(user) FROM INFORMATION_SCHEMA.SYSTEM_USERS", # HSQLDB space-LIMIT
"SELECT TOP 1 name FROM master..sysdatabases", # MSSQL TOP + db..table
"CONCAT('\\',0x71,(SELECT 1))", # backslash-literal string (ANSI)
)
# malformed fragments/statements that MUST flag
BAD = (
"(SELECT id 1 FROM users)", # missing separator
"1 AND 1=(SELECT id 1 FROM users' WHERE tablename='foobar')", # stray quote in scope
"1UNION SELECT NULL", # digit glued to word
"1 AND 5108=5108AND 1=1", # digit glued to keyword
"1 AND 1 = = 1", # doubled operator
"1 AND AND 1=1", # doubled keyword operator
"1 UNION SELECT NULL,,NULL", # empty list item
"1 AND (1=1)(2=2)", # adjacent groups
"SELECT count() FROM t WHERE id=)", # operator before ')'
)
# queries.xml: attributes that carry SQL (not regexes/markers)
_SQL_ATTRS = ("query", "query2", "query3", "count", "count2", "count3",
"condition", "condition2", "condition3",
"keyset_where", "keyset_next", "keyset_first", "keyset_by", "keyset_ordered", "rowid")
_SKIP_TAGS = ("limitregexp", "comment")
def _materialize(s):
"""Substitute sqlmap's template markers with structurally-neutral values so a
catalog template becomes lintable SQL (mirrors what agent.py fills in)."""
s = s.replace("[QUERY]", "SELECT 1").replace("[UNION]", "UNION SELECT NULL")
s = s.replace("[INFERENCE]", "1=1")
s = re.sub(r"\[RANDNUM\d*\]", "1", s)
s = re.sub(r"\[RANDSTR\d*\]", "abc", s)
s = s.replace("[SLEEPTIME]", "1").replace("[DELAYED]", "1")
for _ in ("[DELIMITER_START]", "[DELIMITER_STOP]", "[COLSTART]", "[COLSTOP]"):
s = s.replace(_, "0x71")
s = s.replace("[ORIGVALUE]", "1").replace("[CHAR]", "NULL").replace("[GENERIC_SQL_COMMENT]", "-- -")
s = s.replace("[SINGLE_QUOTE]", "'").replace("[DOUBLE_QUOTE]", '"').replace("[DB]", "db")
s = s.replace("[SPACE_REPLACE]", " ").replace("[HASH_REPLACE]", "#")
s = s.replace("[DOLLAR_REPLACE]", "$").replace("[AT_REPLACE]", "@")
s = re.sub(r"\[[A-Z][A-Z0-9_]*\]", "1", s)
s = s.replace("%d", "1").replace("%s", "col").replace("%%", "%")
return s
def _queries_atoms():
"""{dbms: sorted list of SQL atom strings} from queries.xml."""
retVal = {}
root = ET.parse(os.path.join(ROOT, "data", "xml", "queries.xml")).getroot()
for dbms in root.iter("dbms"):
atoms = set()
for el in dbms.iter():
if el.tag in _SKIP_TAGS:
continue
for attr in _SQL_ATTRS:
raw = el.get(attr)
if raw and not raw.strip().isdigit():
atoms.add(raw)
retVal[dbms.get("value")] = sorted(atoms)
return retVal
def _payload_atoms():
"""[(file, stype-title, sql)] from data/xml/payloads/*.xml."""
import glob
retVal = []
for path in sorted(glob.glob(os.path.join(ROOT, "data", "xml", "payloads", "*.xml"))):
name = os.path.basename(path)
for test in ET.parse(path).getroot().iter("test"):
title = (test.findtext("title") or "").strip()
for tag in ("payload", "vector", "comparison"):
for el in test.iter(tag):
raw = (el.text or "").strip()
if raw:
retVal.append((name, title, raw))
return retVal
class TestLinterSelf(unittest.TestCase):
def test_well_formed_pass(self):
for sql in GOOD + DIALECT_GOOD:
self.assertEqual(checkSanity(sql), [], "false positive on valid SQL: %r" % sql)
def test_malformed_flag(self):
for sql in BAD:
self.assertTrue(checkSanity(sql), "missed malformed SQL: %r" % sql)
# module-level coverage accounting (printed in tearDownModule)
_COVERAGE = {"queries_dbms": 0, "queries_atoms": 0, "payload_atoms": 0}
class TestCatalogCoverage(unittest.TestCase):
def test_queries_xml_clean(self):
atoms = _queries_atoms()
_COVERAGE["queries_dbms"] = len(atoms)
total = 0
for dbms, items in atoms.items():
for raw in items:
total += 1
issues = checkSanity(_materialize(raw))
self.assertEqual(issues, [],
"queries.xml [%s] malformed atom: %r -> %s" % (dbms, raw, issues))
_COVERAGE["queries_atoms"] = total
self.assertGreater(total, 1000) # sanity: the catalog was actually walked
def test_payloads_xml_clean(self):
items = _payload_atoms()
_COVERAGE["payload_atoms"] = len(items)
for name, title, raw in items:
issues = checkSanity(_materialize(raw))
self.assertEqual(issues, [],
"%s malformed payload atom (%s): %r -> %s" % (name, title, raw, issues))
self.assertGreater(len(items), 500)
def tearDownModule():
q = _COVERAGE
total = q["queries_atoms"] + q["payload_atoms"]
sys.stderr.write(
"\n[sqllint coverage] atom-level: %d SQL atoms linted clean "
"(%d queries.xml across %d/30 DBMSes + %d payloads.xml vectors)\n"
% (total, q["queries_atoms"], q["queries_dbms"], q["payload_atoms"]))
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -1,121 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Locks the RestrictedUnpickler security control (lib/core/patch.py, installed over
pickle.loads by dirtyPatches()). sqlmap deserializes pickled blobs out of its own
session DB / cache, so the unpickler is an ALLOWLIST: only safe builtin data types
and sqlmap's own (lib/plugins/thirdparty) classes may be reconstructed.
Two directions, both of which must keep holding:
- LEGIT round-trips sqlmap actually relies on (AttribDict, BigArray, nested
builtins, and - the easy-to-regress one - bytes under PICKLE_PROTOCOL=2, which
emits a _codecs.encode global) must survive base64pickle -> base64unpickle.
- MALICIOUS / exotic globals (eval, os.system, subprocess.Popen, importlib,
operator.attrgetter, and even the non-whitelisted _codecs.lookup) must be
REJECTED at find_class time, before the object is ever built.
A regression in either direction is a security or a data-loss bug, hence the test.
"""
import os
import pickle
import subprocess
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap() # installs dirtyPatches(), i.e. the RestrictedUnpickler over pickle.loads
from lib.core.bigarray import BigArray
from lib.core.convert import base64pickle, base64unpickle, encodeBase64
from lib.core.datatype import AttribDict
from lib.core.settings import PICKLE_PROTOCOL
class _EvilReduce(object):
"""On unpickling, __reduce__ asks the loader to resolve (and would call) an arbitrary global."""
def __init__(self, func, args):
self._func = func
self._args = args
def __reduce__(self):
return (self._func, self._args)
def _payload(func, *args):
# built with the REAL pickler (only pickle.loads is restricted, not dumps); base64 to mirror
# exactly what base64unpickle() consumes from sqlmap's session store
return encodeBase64(pickle.dumps(_EvilReduce(func, args), PICKLE_PROTOCOL), binary=False)
class TestUnpicklerIsInstalled(unittest.TestCase):
def test_patch_active(self):
# if this is False the whole allowlist is bypassed and the negative tests would pass vacuously
self.assertTrue(getattr(pickle, "_patched", False))
class TestLegitRoundTrips(unittest.TestCase):
def _roundtrip(self, value):
return base64unpickle(base64pickle(value))
def test_nested_builtins(self):
value = {"a": [1, 2.5, True, None, complex(1, 2)], "b": (u"x", b"y"), "c": {3, 4}, "d": frozenset([5])}
self.assertEqual(self._roundtrip(value), value)
def test_bytes_protocol2(self):
# protocol-2 pickling of bytes on Python 3 emits a _codecs.encode global; this is the
# exact case the allowlist explicitly permits, and the one most likely to silently break
for value in (b"", b"\x00\x01\x02binary\xff", bytearray(b"abc")):
self.assertEqual(self._roundtrip(value), value)
def test_attribdict(self):
value = AttribDict()
value.foo = "bar"
value.nested = {"k": [1, 2]}
restored = self._roundtrip(value)
self.assertIsInstance(restored, AttribDict)
self.assertEqual(restored.foo, "bar")
self.assertEqual(restored.nested, {"k": [1, 2]})
def test_bigarray(self):
restored = self._roundtrip(BigArray([1, 2, 3]))
self.assertIsInstance(restored, BigArray)
self.assertEqual(list(restored), [1, 2, 3])
class TestMaliciousRejected(unittest.TestCase):
def _assert_blocked(self, payload):
# find_class() raises ValueError; base64unpickle only swallows TypeError, so it propagates
self.assertRaises(ValueError, base64unpickle, payload)
def test_dangerous_builtins(self):
# builtins are allowed ONLY for the safe data-type subset; callables must be refused
for func in (eval, getattr, __import__):
self._assert_blocked(_payload(func, "1+1") if func is eval else _payload(func, "x"))
def test_os_system(self):
self._assert_blocked(_payload(os.system, "echo pwned"))
def test_subprocess_popen(self):
self._assert_blocked(_payload(subprocess.Popen, "echo pwned"))
def test_importlib(self):
import importlib
self._assert_blocked(_payload(importlib.import_module, "os"))
def test_operator_attrgetter(self):
import operator
self._assert_blocked(_payload(operator.attrgetter, "system"))
def test_codecs_lookup_not_whitelisted(self):
# only _codecs.encode is allowed (for the bytes round-trip); every other _codecs name stays blocked
import codecs
self._assert_blocked(_payload(codecs.lookup, "utf-8"))
if __name__ == "__main__":
unittest.main()

View file

@ -96,7 +96,6 @@ if py3k:
from http.cookies import SimpleCookie, Morsel, CookieError
from collections.abc import MutableMapping as DictMixin
from types import ModuleType as new_module
import pickle
from io import BytesIO
import configparser
from datetime import timezone
@ -125,7 +124,6 @@ else: # 2.x
from urllib import urlencode, quote as urlquote, unquote as urlunquote
from Cookie import SimpleCookie, Morsel, CookieError
from itertools import imap
import cPickle as pickle
from imp import new_module
from StringIO import StringIO as BytesIO
import ConfigParser as configparser
@ -1226,7 +1224,7 @@ class BaseRequest(object):
sig, msg = map(tob, value[1:].split('?', 1))
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
if _lscmp(sig, base64.b64encode(hash)):
dst = pickle.loads(base64.b64decode(msg))
dst = json_loads(base64.b64decode(msg))
if dst and dst[0] == key:
return dst[1]
return default
@ -1839,14 +1837,13 @@ class BaseResponse(object):
expire at the end of the browser session (as soon as the browser
window is closed).
Signed cookies may store any pickle-able object and are
Signed cookies may store any JSON-serializable object and are
cryptographically signed to prevent manipulation. Keep in mind that
cookies are limited to 4kb in most browsers.
Warning: Pickle is a potentially dangerous format. If an attacker
gains access to the secret key, he could forge cookies that execute
code on server side if unpickled. Using pickle is discouraged and
support for it will be removed in later versions of bottle.
Warning: If an attacker gains access to the secret key, he could
forge arbitrary cookies. Prefer storing only plain strings and, if
possible, keep session data server-side instead of in cookies.
Warning: Signed cookies are not encrypted (the client can still see
the content) and not copy-protected (the client can restore an old
@ -1863,10 +1860,10 @@ class BaseResponse(object):
if secret:
if not isinstance(value, basestring):
depr(0, 13, "Pickling of arbitrary objects into cookies is "
depr(0, 13, "Storing non-string objects into cookies is "
"deprecated.", "Only store strings in cookies. "
"JSON strings are fine, too.")
encoded = base64.b64encode(pickle.dumps([name, value], -1))
encoded = base64.b64encode(tob(json_dumps([name, value])))
sig = base64.b64encode(hmac.new(tob(secret), encoded,
digestmod=digestmod).digest())
value = touni(tob('!') + sig + tob('?') + encoded)
@ -2253,7 +2250,7 @@ class FormsDict(MultiDict):
return default
def __getattr__(self, name, default=unicode()):
# Without this guard, pickle generates a cryptic TypeError:
# Without this guard, dunder attribute probing generates a cryptic TypeError:
if name.startswith('__') and name.endswith('__'):
return super(FormsDict, self).__getattr__(name)
return self.getunicode(name, default=default)
@ -3069,11 +3066,11 @@ def _lscmp(a, b):
def cookie_encode(data, key, digestmod=None):
""" Encode and sign a pickle-able object. Return a (byte) string """
""" Encode and sign a JSON-serializable object. Return a (byte) string """
depr(0, 13, "cookie_encode() will be removed soon.",
"Do not use this API directly.")
digestmod = digestmod or hashlib.sha256
msg = base64.b64encode(pickle.dumps(data, -1))
msg = base64.b64encode(tob(json_dumps(data)))
sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest())
return tob('!') + sig + tob('?') + msg
@ -3088,7 +3085,7 @@ def cookie_decode(data, key, digestmod=None):
digestmod = digestmod or hashlib.sha256
hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest()
if _lscmp(sig[1:], base64.b64encode(hashed)):
return pickle.loads(base64.b64decode(msg))
return json_loads(base64.b64decode(msg))
return None