Repository: jsonpickle/jsonpickle
Pull request #545 (2024-12-13 04:32:02)

jsonpickle: guard against invalid b85 and b64 data

davvid: I'll add a changelog entry tonight once I'm back home.
davvid: I found a few more out-of-band values that could trigger other errors so I amended this commit with  more tests and exception cases to cover these values for b64 and b85. We have a number of unreleased fuzz fixes at this point so I'm going to deploy a patch release today.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index f9a43ab..81b125a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,7 +1,7 @@
 Upcoming
 ========
     * The unpickler is now more resilient to malformed "py/reduce", "py/set",
-      "py/tuple", and "py/iterator" input data. (+544)
+      "py/tuple", "py/b64", "py/b85", and "py/iterator" input data. (+544) (+545)
     * The test suite was updated to leverage more pytest features.
     * The ``jsonpickle.compat`` module is no longer used. It is still provided
       for backwards compatibility but it may be removed in a future version.
diff --git a/jsonpickle/unpickler.py b/jsonpickle/unpickler.py
index ea74a20..3ea9ade 100644
--- a/jsonpickle/unpickler.py
+++ b/jsonpickle/unpickler.py
@@ -433,10 +433,16 @@ class Unpickler:
             self._classes[util.importable_name(classes)] = classes
 
     def _restore_base64(self, obj):
-        return util.b64decode(obj[tags.B64].encode('utf-8'))
+        try:
+            return util.b64decode(obj[tags.B64].encode('utf-8'))
+        except AttributeError:
+            return b''
 
     def _restore_base85(self, obj):
-        return util.b85decode(obj[tags.B85].encode('utf-8'))
+        try:
+            return util.b85decode(obj[tags.B85].encode('utf-8'))
+        except AttributeError:
+            return b''
 
     def _refname(self):
         """Calculates the name of the current location in the JSON stack.
diff --git a/jsonpickle/util.py b/jsonpickle/util.py
index 5e5699e..18d951b 100644
--- a/jsonpickle/util.py
+++ b/jsonpickle/util.py
@@ -9,6 +9,7 @@
 determining the type of an object.
 """
 import base64
+import binascii
 import collections
 import inspect
 import io
@@ -557,7 +558,10 @@ def b64decode(payload):
     """
     Decode payload - must be ascii text.
     """
-    return base64.b64decode(payload)
+    try:
+        return base64.b64decode(payload)
+    except (TypeError, binascii.Error):
+        return b''
 
 
 def b85encode(data):
@@ -571,7 +575,10 @@ def b85decode(payload):
     """
     Decode payload - must be ascii text.
     """
-    return base64.b85decode(payload)
+    try:
+        return base64.b85decode(payload)
+    except (TypeError, ValueError):
+        return b''
 
 
 def itemgetter(obj, getter=operator.itemgetter(0)):


Test patch:
diff --git a/tests/jsonpickle_test.py b/tests/jsonpickle_test.py
index befc536..d438827 100644
--- a/tests/jsonpickle_test.py
+++ b/tests/jsonpickle_test.py
@@ -217,6 +217,14 @@ def test_decode_base85(unpickler):
     assert unpickler.restore(pickled) == expected
 
 
+@pytest.mark.parametrize('value', ['', '/', 1, True, False, None, [], {}])
+def test_decode_invalid_b85(value, unpickler):
+    """Invalid base85 data restores to an empty string"""
+    expected = b''
+    pickled = {tags.B85: value}
+    assert unpickler.restore(pickled) == expected
+
+
 def test_base85_still_handles_base64(unpickler):
     """base64 must be restored even though base85 is the default"""
     expected = 'Pÿthöñ 3!'.encode()
@@ -224,6 +232,14 @@ def test_base85_still_handles_base64(unpickler):
     assert unpickler.restore(pickled) == expected
 
 
+@pytest.mark.parametrize('value', ['', 'x', '!', 0, 1, True, False, None, [], {}])
+def test_decode_invalid_b64(value, unpickler):
+    """Invalid base85 data restores to an empty string"""
+    expected = b''
+    pickled = {tags.B64: value}
+    assert unpickler.restore(pickled) == expected
+
+
 def test_string(pickler, unpickler):
     """Strings must roundtrip"""
     assert pickler.flatten('a string') == 'a string'