diff --git a/lib/core/settings.py b/lib/core/settings.py index d71fcc304..71e01889f 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.125" +VERSION = "1.10.7.126" 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) diff --git a/lib/utils/har.py b/lib/utils/har.py index e5dde561c..ebdef3bfd 100644 --- a/lib/utils/har.py +++ b/lib/utils/har.py @@ -181,16 +181,22 @@ class Response(object): def toDict(self): content = { "mimeType": self.headers.get("Content-Type"), - "text": self.content, "size": len(self.content or "") } - binary = set([b'\0', b'\1', u'\0', u'\1', 0, 1]) - if any(c in binary for c in self.content): - content["encoding"] = "base64" - content["text"] = getText(base64.b64encode(self.content)) + # HAR text must be UTF-8: a body that does not decode (binary content such as an image, or + # text in another charset) is base64-encoded losslessly rather than mangled through a lossy + # text decode. The previous check only treated NUL/0x01 as binary, so e.g. a JPEG lacking + # those bytes was corrupted into the "text" field. + raw = self.content or b"" + if not isinstance(raw, bytes): + content["text"] = getText(raw) else: - content["text"] = getText(content["text"]) + try: + content["text"] = getText(raw.decode("utf-8")) + except UnicodeDecodeError: + content["encoding"] = "base64" + content["text"] = getText(base64.b64encode(raw)) return { "httpVersion": self.httpVersion, diff --git a/tests/test_har.py b/tests/test_har.py index 56e9b69b5..bf98f2608 100644 --- a/tests/test_har.py +++ b/tests/test_har.py @@ -160,6 +160,23 @@ class TestResponse(unittest.TestCase): d = resp.toDict() self.assertEqual(d["content"]["encoding"], "base64") + def test_toDict_binary_without_null_still_base64(self): + # regression: binary content lacking NUL/0x01 (e.g. a JPEG header) must still be + # base64-encoded, not mangled into the "text" field as a lossy decode + import base64 as _b64 + jpeg = b"\xff\xd8\xff\xe0\x10JFIF\x02\x03" + d = H.Response("HTTP/1.1", 200, "OK", {"Content-Type": "image/jpeg"}, jpeg).toDict() + self.assertEqual(d["content"]["encoding"], "base64") + self.assertEqual(_b64.b64decode(d["content"]["text"]), jpeg) # lossless + + def test_toDict_utf8_multibyte_stays_text(self): + # valid UTF-8 (incl. multibyte) is human-readable text, not base64 + original = b"caf\xc3\xa9 \xe4\xbd\xa0\xe5\xa5\xbd".decode("utf-8") # cafe+e-acute+two CJK, pure-ASCII source + d = H.Response("HTTP/1.1", 200, "OK", {"Content-Type": "text/plain; charset=utf-8"}, + original.encode("utf-8")).toDict() + self.assertNotIn("encoding", d["content"]) + self.assertEqual(d["content"]["text"], original) + def test_toDict_non_text_content(self): resp = H.Response("HTTP/1.1", 200, "OK", {"Content-Type": "text/plain"}, b"plain text")