Skip to content

Commit 4504eda

Browse files
committed
Switching from pickle to json for serialization
1 parent e2c55c1 commit 4504eda

12 files changed

Lines changed: 934 additions & 274 deletions

File tree

lib/core/bigarray.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@
55
See the file 'LICENSE' for copying permission
66
"""
77

8-
try:
9-
import cPickle as pickle
10-
except:
11-
import pickle
12-
138
import itertools
149
import os
1510
import shutil
@@ -86,7 +81,7 @@ class BigArray(list):
8681
>>> _[0] = [None]
8782
>>> _.index(0)
8883
20
89-
>>> import pickle; __ = pickle.loads(pickle.dumps(_))
84+
>>> import copy; __ = copy.deepcopy(_)
9085
>>> __.append(1)
9186
>>> len(_)
9287
100001
@@ -158,9 +153,10 @@ def pop(self):
158153
self.chunks[-1] = self.cache.data
159154
self.cache.dirty = False
160155
else:
156+
from lib.core.convert import deserializeValue
161157
try:
162158
with open(filename, "rb") as f:
163-
self.chunks[-1] = pickle.loads(zlib.decompress(f.read()))
159+
self.chunks[-1] = deserializeValue(zlib.decompress(f.read()))
164160
except IOError as ex:
165161
errMsg = "exception occurred while retrieving data "
166162
errMsg += "from a temporary file ('%s')" % ex
@@ -207,11 +203,13 @@ def __del__(self):
207203
self.close()
208204

209205
def _dump(self, chunk):
206+
from lib.core.convert import getBytes, serializeValue
210207
try:
211208
handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.BIG_ARRAY)
212209
self.filenames.add(filename)
213210
with os.fdopen(handle, "w+b") as f:
214-
f.write(zlib.compress(pickle.dumps(chunk, pickle.HIGHEST_PROTOCOL), BIGARRAY_COMPRESS_LEVEL))
211+
# serializeValue() returns text; encode to bytes for the compressed on-disk chunk
212+
f.write(zlib.compress(getBytes(serializeValue(chunk)), BIGARRAY_COMPRESS_LEVEL))
215213
return filename
216214
except (OSError, IOError) as ex:
217215
errMsg = "exception occurred while storing data "
@@ -240,9 +238,10 @@ def _checkcache(self, index):
240238
pass
241239

242240
if not (self.cache and self.cache.index == index):
241+
from lib.core.convert import deserializeValue
243242
try:
244243
with open(self.chunks[index], "rb") as f:
245-
self.cache = Cache(index, pickle.loads(zlib.decompress(f.read())), False)
244+
self.cache = Cache(index, deserializeValue(zlib.decompress(f.read())), False)
246245
except Exception as ex:
247246
errMsg = "exception occurred while retrieving data "
248247
errMsg += "from a temporary file ('%s')" % ex
@@ -361,8 +360,9 @@ def __iter__(self):
361360
if cache_index == idx and cache_data is not None:
362361
data = cache_data
363362
else:
363+
from lib.core.convert import deserializeValue
364364
with open(chunk, "rb") as f:
365-
data = pickle.loads(zlib.decompress(f.read()))
365+
data = deserializeValue(zlib.decompress(f.read()))
366366
except Exception as ex:
367367
errMsg = "exception occurred while retrieving data "
368368
errMsg += "from a temporary file ('%s')" % ex

lib/core/common.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@
5353
from lib.core.compat import RecursionError
5454
from lib.core.compat import round
5555
from lib.core.compat import xrange
56-
from lib.core.convert import base64pickle
57-
from lib.core.convert import base64unpickle
5856
from lib.core.convert import decodeBase64
57+
from lib.core.convert import deserializeValue
5958
from lib.core.convert import decodeHex
6059
from lib.core.convert import getBytes
6160
from lib.core.convert import getText
6261
from lib.core.convert import getUnicode
6362
from lib.core.convert import htmlUnescape
63+
from lib.core.convert import serializeValue
6464
from lib.core.convert import stdoutEncode
6565
from lib.core.data import cmdLineOptions
6666
from lib.core.data import conf
@@ -5071,19 +5071,19 @@ def serializeObject(object_):
50715071
True
50725072
"""
50735073

5074-
return base64pickle(object_)
5074+
return serializeValue(object_)
50755075

50765076
def unserializeObject(value):
50775077
"""
50785078
Unserializes object from given serialized form
50795079
50805080
>>> unserializeObject(serializeObject([1, 2, 3])) == [1, 2, 3]
50815081
True
5082-
>>> unserializeObject('gAJVBmZvb2JhcnEBLg==')
5083-
'foobar'
5082+
>>> unserializeObject(serializeObject('foobar')) == 'foobar'
5083+
True
50845084
"""
50855085

5086-
return base64unpickle(value) if value else None
5086+
return deserializeValue(value) if value else None
50875087

50885088
def resetCounter(technique):
50895089
"""

lib/core/convert.py

Lines changed: 208 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
See the file 'LICENSE' for copying permission
66
"""
77

8-
try:
9-
import cPickle as pickle
10-
except:
11-
import pickle
12-
138
import base64
149
import binascii
1510
import codecs
11+
import datetime
12+
import decimal
1613
import json
1714
import re
1815
import sys
@@ -26,7 +23,6 @@
2623
from lib.core.settings import IS_TTY
2724
from lib.core.settings import IS_WIN
2825
from lib.core.settings import NULL
29-
from lib.core.settings import PICKLE_PROTOCOL
3026
from lib.core.settings import SAFE_HEX_MARKER
3127
from lib.core.settings import UNICODE_ENCODING
3228
from thirdparty import six
@@ -41,48 +37,228 @@
4137

4238
htmlEscape = _escape
4339

44-
def base64pickle(value):
40+
# Safe (no arbitrary code execution) serialization used for the session store (HashDB)
41+
# and BigArray disk chunks. The former serializer could execute code while loading, so
42+
# deserializing sqlmap's own (locally writable) session/cache files was a recurring
43+
# report magnet. This codec serializes to plain JSON with explicit type tags, so nothing
44+
# is ever executed on load.
45+
#
46+
# JSON natively covers only str/int/float/bool/None/list, and silently loses the rest
47+
# (int/tuple dict keys become strings, set/tuple/bytes are rejected). The tagged wrappers
48+
# below preserve every type sqlmap actually stores: bytes, tuple, set/frozenset, dict with
49+
# arbitrary (non-string) keys, DB-driver scalars (Decimal/datetime/...), and the handful of
50+
# sqlmap's own classes below. Reconstruction of classes is limited to that explicit
51+
# allowlist (no module/namespace wildcard), so no dangerous callable is ever reachable.
52+
53+
# reserved wrapper key; data mappings are encoded as tagged pair-lists (never as bare JSON
54+
# objects), so any decoded JSON object is one of our wrappers and this key can never collide
55+
_SERIALIZE_TAG = "$T"
56+
57+
# fully-qualified names of the ONLY classes that may be reconstructed on deserialization
58+
_SERIALIZE_CLASSES = frozenset((
59+
"lib.core.datatype.AttribDict",
60+
"lib.core.datatype.InjectionDict",
61+
"lib.utils.har.RawPair",
62+
))
63+
64+
def _serializeEncode(value):
4565
"""
46-
Serializes (with pickle) and encodes to Base64 format supplied (binary) value
47-
48-
>>> base64unpickle(base64pickle([1, 2, 3])) == [1, 2, 3]
49-
True
50-
>>> isinstance(base64unpickle(base64pickle(BigArray([1, 2, 3]))), BigArray)
51-
True
66+
Turns a Python value into a JSON-serializable (tagged) structure
5267
"""
5368

54-
retVal = None
69+
if value is None or isinstance(value, bool) or isinstance(value, float) or isinstance(value, six.integer_types):
70+
return value
71+
72+
if isinstance(value, six.text_type):
73+
return value
74+
75+
# Note: on Python 2 'str' is binary; base64-tagging it (rather than emitting a native JSON
76+
# string that would round-trip as 'unicode') keeps the exact byte type across versions
77+
if isinstance(value, (six.binary_type, bytearray)):
78+
raw = bytes(value) if isinstance(value, bytearray) else value
79+
return {_SERIALIZE_TAG: "b", "v": encodeBase64(raw, binary=False), "a": 1 if isinstance(value, bytearray) else 0}
80+
81+
if isinstance(value, memoryview):
82+
return {_SERIALIZE_TAG: "b", "v": encodeBase64(value.tobytes(), binary=False), "a": 0}
5583

5684
try:
57-
retVal = encodeBase64(pickle.dumps(value, PICKLE_PROTOCOL), binary=False)
58-
except:
59-
warnMsg = "problem occurred while serializing "
60-
warnMsg += "instance of a type '%s'" % type(value)
61-
singleTimeWarnMessage(warnMsg)
85+
if isinstance(value, buffer): # noqa: F821 # Python 2 only
86+
return {_SERIALIZE_TAG: "b", "v": encodeBase64(bytes(value), binary=False), "a": 0}
87+
except NameError:
88+
pass
89+
90+
# Note: BigArray is a 'list' subclass, so it must be matched before the plain-list branch
91+
# (otherwise it would round-trip as a plain list, losing its type)
92+
if isinstance(value, BigArray):
93+
return {_SERIALIZE_TAG: "ba", "v": [_serializeEncode(_) for _ in value]}
94+
95+
if isinstance(value, list):
96+
return [_serializeEncode(_) for _ in value]
97+
98+
if isinstance(value, tuple):
99+
return {_SERIALIZE_TAG: "t", "v": [_serializeEncode(_) for _ in value]}
100+
101+
if isinstance(value, frozenset):
102+
return {_SERIALIZE_TAG: "f", "v": [_serializeEncode(_) for _ in value]}
103+
104+
if isinstance(value, (set, _collections.Set)):
105+
return {_SERIALIZE_TAG: "s", "v": [_serializeEncode(_) for _ in value]}
106+
107+
if isinstance(value, dict):
108+
name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__)
109+
if name in _SERIALIZE_CLASSES:
110+
return {_SERIALIZE_TAG: "o", "c": name, "d": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()], "s": _serializeEncode(dict(value.__dict__))}
111+
elif value.__class__ is dict:
112+
return {_SERIALIZE_TAG: "m", "v": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()]}
113+
else:
114+
return _serializeUnknown(value, name)
62115

63-
try:
64-
retVal = encodeBase64(pickle.dumps(value), binary=False)
65-
except:
66-
raise
116+
if isinstance(value, decimal.Decimal):
117+
return {_SERIALIZE_TAG: "dec", "v": getUnicode(value)}
118+
119+
if isinstance(value, datetime.datetime):
120+
return {_SERIALIZE_TAG: "dt", "v": [value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond]}
121+
122+
if isinstance(value, datetime.date):
123+
return {_SERIALIZE_TAG: "date", "v": [value.year, value.month, value.day]}
124+
125+
if isinstance(value, datetime.time):
126+
return {_SERIALIZE_TAG: "time", "v": [value.hour, value.minute, value.second, value.microsecond]}
127+
128+
if isinstance(value, datetime.timedelta):
129+
return {_SERIALIZE_TAG: "td", "v": [value.days, value.seconds, value.microseconds]}
130+
131+
name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__)
132+
if name in _SERIALIZE_CLASSES:
133+
return {_SERIALIZE_TAG: "o", "c": name, "s": _serializeEncode(dict(value.__dict__))}
134+
135+
return _serializeUnknown(value, name)
136+
137+
def _serializeUnknown(value, name):
138+
"""
139+
Fallback for a type not explicitly handled by the serializer
140+
"""
141+
142+
# sqlmap's own (or bundled) classes MUST be added to the allowlist explicitly - fail loudly
143+
# (caught by the regression tests) rather than silently store something that cannot be restored
144+
if (name or "").split(".")[0] in ("lib", "plugins", "thirdparty"):
145+
raise TypeError("serialization of type '%s' is not supported" % name)
146+
147+
# a foreign/exotic scalar (e.g. an unusual DB-driver value): degrade to its textual form rather
148+
# than crash a user's session - session values are only ever rendered (getUnicode) downstream
149+
singleTimeWarnMessage("serializing value of unsupported type '%s' as text" % name)
150+
return getUnicode(value)
151+
152+
def _serializeDecode(struct):
153+
"""
154+
Restores a Python value from a JSON-deserialized (tagged) structure
155+
"""
156+
157+
if struct is None or isinstance(struct, bool) or isinstance(struct, float) or isinstance(struct, six.integer_types):
158+
return struct
159+
160+
if isinstance(struct, six.text_type):
161+
return struct
162+
163+
if isinstance(struct, six.binary_type): # defensive - json.loads() yields text, not bytes
164+
return getUnicode(struct)
165+
166+
if isinstance(struct, list):
167+
return [_serializeDecode(_) for _ in struct]
168+
169+
if isinstance(struct, dict):
170+
tag = struct.get(_SERIALIZE_TAG)
171+
172+
if tag == "b":
173+
raw = decodeBase64(struct["v"], binary=True)
174+
return bytearray(raw) if struct.get("a") else raw
175+
elif tag == "t":
176+
return tuple(_serializeDecode(_) for _ in struct["v"])
177+
elif tag == "f":
178+
return frozenset(_serializeDecode(_) for _ in struct["v"])
179+
elif tag == "ba":
180+
return BigArray([_serializeDecode(_) for _ in struct["v"]])
181+
elif tag == "s":
182+
return set(_serializeDecode(_) for _ in struct["v"])
183+
elif tag == "m":
184+
return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct["v"])
185+
elif tag == "dec":
186+
return decimal.Decimal(struct["v"])
187+
elif tag == "dt":
188+
return datetime.datetime(*struct["v"])
189+
elif tag == "date":
190+
return datetime.date(*struct["v"])
191+
elif tag == "time":
192+
return datetime.time(*struct["v"])
193+
elif tag == "td":
194+
return datetime.timedelta(struct["v"][0], struct["v"][1], struct["v"][2])
195+
elif tag == "o":
196+
return _serializeDecodeObject(struct)
197+
elif tag is None: # defensive - a bare mapping should never occur
198+
return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct.items())
199+
else:
200+
raise ValueError("unsupported serialized tag '%s'" % tag)
201+
202+
raise ValueError("unsupported serialized structure of type '%s'" % type(struct))
203+
204+
def _serializeResolveClass(name):
205+
"""
206+
Resolves an allowlisted class name to its class (nothing else may be reconstructed)
207+
"""
208+
209+
if name not in _SERIALIZE_CLASSES:
210+
raise ValueError("deserialization of class '%s' is forbidden" % name)
211+
212+
if name == "lib.utils.har.RawPair":
213+
from lib.utils.har import RawPair
214+
return RawPair
215+
else:
216+
from lib.core.datatype import AttribDict, InjectionDict
217+
return InjectionDict if name.endswith("InjectionDict") else AttribDict
218+
219+
def _serializeDecodeObject(struct):
220+
"""
221+
Reconstructs an allowlisted class instance from its serialized form
222+
"""
223+
224+
_class = _serializeResolveClass(struct.get("c"))
225+
retVal = _class.__new__(_class)
226+
227+
if isinstance(retVal, dict):
228+
for pair in (struct.get("d") or []):
229+
dict.__setitem__(retVal, _serializeDecode(pair[0]), _serializeDecode(pair[1]))
230+
231+
state = _serializeDecode(struct.get("s") or {})
232+
if isinstance(state, dict):
233+
retVal.__dict__.update(state)
67234

68235
return retVal
69236

70-
def base64unpickle(value):
237+
def serializeValue(value):
71238
"""
72-
Decodes value from Base64 to plain format and deserializes (with pickle) its content
239+
Safely serializes a Python value to its canonical serialized form (JSON text), without any
240+
code-execution risk
73241
74-
>>> type(base64unpickle('gAJjX19idWlsdGluX18Kb2JqZWN0CnEBKYFxAi4=')) == object
242+
Note: the output is pure ASCII text, so it is stored verbatim in the (TEXT) session store - no
243+
Base64 (or any base-N) wrapping is needed (that was only required by the former binary
244+
serialization), which also keeps the stored form as small as possible. Callers that need raw
245+
bytes (e.g. a compressed BigArray disk chunk) simply encode the returned text.
246+
247+
>>> deserializeValue(serializeValue({1: 'a', 'b': (2, 3), 'c': {4, 5}})) == {1: 'a', 'b': (2, 3), 'c': {4, 5}}
248+
True
249+
>>> deserializeValue(serializeValue([1, 2, (3, {4: b'5'})])) == [1, 2, (3, {4: b'5'})]
75250
True
76251
"""
77252

78-
retVal = None
253+
return json.dumps(_serializeEncode(value), ensure_ascii=True, separators=(',', ':'))
79254

80-
try:
81-
retVal = pickle.loads(decodeBase64(value))
82-
except TypeError:
83-
retVal = pickle.loads(decodeBase64(bytes(value)))
255+
def deserializeValue(value):
256+
"""
257+
Restores a Python value from its serialized form (accepts the serialized data as either text or
258+
bytes)
259+
"""
84260

85-
return retVal
261+
return _serializeDecode(json.loads(getText(value)))
86262

87263
def htmlUnescape(value):
88264
"""

0 commit comments

Comments
 (0)