CVE-2017-20189: Deserialization of Untrusted Data (CWE-502)
value: In Clojure before 1.9.0, classes can be used to construct a serialized object that executes arbitrary code upon deserialization. This is relevant if a server deserializes untrusted objects.
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 9.8
Published: 2024-01-22T06:15Z
Repository: https://github.com/clojure/clojure
Language: Other
Files changed: src/clj/clojure/core_proxy.clj, test/clojure/test_clojure/java_interop.clj
Commit date: 2017-09-06 12:25:37 -0400
Commit message: CLJ-2204 Disable serialization of proxy classes

Signed-off-by: Stuart Halloway <stu@cognitect.com>

Diff: diff --git a/src/clj/clojure/core_proxy.clj b/src/clj/clojure/core_proxy.clj
index 813c8bbe..70d0528f 100644
--- a/src/clj/clojure/core_proxy.clj
+++ b/src/clj/clojure/core_proxy.clj
@@ -11,10 +11,11 @@
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;; proxy ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 (import
  '(clojure.asm ClassWriter ClassVisitor Opcodes Type) 
  '(java.lang.reflect Modifier Constructor)
+ '(java.io Serializable NotSerializableException)
  '(clojure.asm.commons Method GeneratorAdapter)
  '(clojure.lang IProxy Reflector DynamicClassLoader IPersistentMap PersistentHashMap RT))
 
 (defn method-sig [^java.lang.reflect.Method meth]
   [(. meth (getName)) (seq (. meth (getParameterTypes))) (. meth getReturnType)])
@@ -42,11 +43,12 @@
           (map #(subs % (inc (.lastIndexOf ^String % "."))) inames)
           [(Integer/toHexString (hash inames))])))))
 
 (defn- generate-proxy [^Class super interfaces]
   (let [cv (new ClassWriter (. ClassWriter COMPUTE_MAXS))
-        cname (.replace (proxy-name super interfaces) \. \/) ;(str "clojure/lang/" (gensym "Proxy__"))
+        pname (proxy-name super interfaces)
+        cname (.replace pname \. \/) ;(str "clojure/lang/" (gensym "Proxy__"))
         ctype (. Type (getObjectType cname))
         iname (fn [^Class c] (.. Type (getType c) (getInternalName)))
         fmap "__clojureFnMap"
         totype (fn [^Class c] (. Type (getType c)))
         to-types (fn [cs] (if (pos? (count cs))
@@ -146,10 +148,26 @@
             (. gen (loadArgs))
             (. gen (invokeConstructor super-type m))
             
             (. gen (returnValue))
             (. gen (endMethod)))))
+                                        ;disable serialization
+    (when (some #(isa? % Serializable) (cons super interfaces))
+      (let [m (. Method (getMethod "void writeObject(java.io.ObjectOutputStream)"))
+            gen (new GeneratorAdapter (. Opcodes ACC_PRIVATE) m nil nil cv)]
+        (. gen (visitCode))
+        (. gen (loadThis))
+        (. gen (loadArgs))
+        (. gen (throwException (totype NotSerializableException) pname))
+        (. gen (endMethod)))
+      (let [m (. Method (getMethod "void readObject(java.io.ObjectInputStream)"))
+            gen (new GeneratorAdapter (. Opcodes ACC_PRIVATE) m nil nil cv)]
+        (. gen (visitCode))
+        (. gen (loadThis))
+        (. gen (loadArgs))
+        (. gen (throwException (totype NotSerializableException) pname))
+        (. gen (endMethod))))
                                         ;add IProxy methods
     (let [m (. Method (getMethod "void __initClojureFnMappings(clojure.lang.IPersistentMap)"))
           gen (new GeneratorAdapter (. Opcodes ACC_PUBLIC) m nil nil cv)]
       (. gen (visitCode))
       (. gen (loadThis))
diff --git a/test/clojure/test_clojure/java_interop.clj b/test/clojure/test_clojure/java_interop.clj
index 86ba5ca6..44b5c707 100644
--- a/test/clojure/test_clojure/java_interop.clj
+++ b/test/clojure/test_clojure/java_interop.clj
@@ -8,11 +8,12 @@
 
 ; Author: Frantisek Sodomka
 
 
 (ns clojure.test-clojure.java-interop
-  (:use clojure.test))
+  (:use clojure.test)
+  (:require [clojure.inspector]))
 
 ; http://clojure.org/java_interop
 ; http://clojure.org/compilation
 
 
@@ -169,10 +170,38 @@
             (update-proxy {"toString" (fn [_] "chain chain chain")}) 
             str)
         "chain chain chain")))
 
 
+;; serialized-proxy can be regenerated using a modified version of
+;; Clojure with the proxy serialization prohibition disabled and the
+;; following code:
+#_(let [baos (java.io.ByteArrayOutputStream.) ]
+    (with-open [baos baos]
+      (.writeObject (java.io.ObjectOutputStream. baos) (clojure.inspector/list-model nil)))
+    (println (apply str (for [c (String. (.toByteArray baos) "ISO-8859-1")]
+                          (if (<= 32 (int c) (int \z)) c (format "\\%03o" (int c)))))))
+(def serialized-proxy "\254\355\000\005sr\000Eclojure.inspector.proxy$javax.swing.table.AbstractTableModel$ff19274art\330\266_\010ME\002\000\001L\000\016__clojureFnMapt\000\035Lclojure/lang/IPersistentMap;xr\000$javax.swing.table.AbstractTableModelr\313\3538\256\001\377\276\002\000\001L\000\014listenerListt\000%Ljavax/swing/event/EventListenerList;xpsr\000#javax.swing.event.EventListenerList\2616\306\175\204\352\326D\003\000\000xppxsr\000\037clojure.lang.PersistentArrayMap\3437p\017\230\305\364\337\002\000\002L\000\005_metaq\000\176\000\001[\000\005arrayt\000\023[Ljava/lang/Object;xr\000\033clojure.lang.APersistentMap]\174/\003t r\173\002\000\002I\000\005_hashI\000\007_hasheqxp\000\000\000\000\000\000\000\000pur\000\023[Ljava.lang.Object;\220\316X\237\020s)l\002\000\000xp\000\000\000\006t\000\016getColumnCountsr\000%clojure.inspector$list_model$fn__8816H\252\320\325b\371!+\002\000\000xr\000\026clojure.lang.AFunction>\006p\234\236F\375\313\002\000\001L\000\021__methodImplCachet\000\036Lclojure/lang/MethodImplCache;xppt\000\013getRowCountsr\000%clojure.inspector$list_model$fn__8818-\037I\247\234/U\226\002\000\001L\000\005nrowst\000\022Ljava/lang/Object;xq\000\176\000\017ppt\000\012getValueAtsr\000%clojure.inspector$list_model$fn__8820\323\331\174ke\233\370\034\002\000\002L\000\011get_labelq\000\176\000\024L\000\011get_valueq\000\176\000\024xq\000\176\000\017ppp")
+
+(deftest test-proxy-non-serializable
+  (testing "That proxy classes refuse serialization and deserialization"
+    ;; Serializable listed directly in interface list:
+    (is (thrown? java.io.NotSerializableException
+                 (-> (java.io.ByteArrayOutputStream.)
+                     (java.io.ObjectOutputStream.)
+                     (.writeObject (proxy [Object java.io.Serializable] [])))))
+    ;; Serializable included via inheritence:
+    (is (thrown? java.io.NotSerializableException
+                 (-> (java.io.ByteArrayOutputStream.)
+                     (java.io.ObjectOutputStream.)
+                     (.writeObject (clojure.inspector/list-model nil)))))
+    ;; Deserialization also prohibited:
+    (is (thrown? java.io.NotSerializableException
+                 (-> serialized-proxy (.getBytes "ISO-8859-1")
+                     java.io.ByteArrayInputStream. java.io.ObjectInputStream.
+                     .readObject)))))
+
 (deftest test-bases
   (are [x y] (= x y)
       (bases java.lang.Math)
         (list java.lang.Object)
       (bases java.util.Collection)

Vulnerable code:         cname (.replace (proxy-name super interfaces) \. \/) ;(str "clojure/lang/" (gensym "Proxy__"))
  (:use clojure.test))
Fixed code:  '(java.io Serializable NotSerializableException)
        pname (proxy-name super interfaces)
        cname (.replace pname \. \/) ;(str "clojure/lang/" (gensym "Proxy__"))
                                        ;disable serialization
    (when (some #(isa? % Serializable) (cons super interfaces))
      (let [m (. Method (getMethod "void writeObject(java.io.ObjectOutputStream)"))
            gen (new GeneratorAdapter (. Opcodes ACC_PRIVATE) m nil nil cv)]
        (. gen (visitCode))
        (. gen (loadThis))
        (. gen (loadArgs))
        (. gen (throwException (totype NotSerializableException) pname))
        (. gen (endMethod)))
      (let [m (. Method (getMethod "void readObject(java.io.ObjectInputStream)"))
            gen (new GeneratorAdapter (. Opcodes ACC_PRIVATE) m nil nil cv)]
        (. gen (visitCode))
        (. gen (loadThis))
        (. gen (loadArgs))
        (. gen (throwException (totype NotSerializableException) pname))
        (. gen (endMethod))))
  (:use clojure.test)
  (:require [clojure.inspector]))
;; serialized-proxy can be regenerated using a modified version of
;; Clojure with the proxy serialization prohibition disabled and the
;; following code:
#_(let [baos (java.io.ByteArrayOutputStream.) ]
    (with-open [baos baos]
      (.writeObject (java.io.ObjectOutputStream. baos) (clojure.inspector/list-model nil)))
    (println (apply str (for [c (String. (.toByteArray baos) "ISO-8859-1")]
                          (if (<= 32 (int c) (int \z)) c (format "\\%03o" (int c)))))))
(def serialized-proxy "\254\355\000\005sr\000Eclojure.inspector.proxy$javax.swing.table.AbstractTableModel$ff19274art\330\266_\010ME\002\000\001L\000\016__clojureFnMapt\000\035Lclojure/lang/IPersistentMap;xr\000$javax.swing.table.AbstractTableModelr\313\3538\256\001\377\276\002\000\001L\000\014listenerListt\000%Ljavax/swing/event/EventListenerList;xpsr\000#javax.swing.event.EventListenerList\2616\306\175\204\352\326D\003\000\000xppxsr\000\037clojure.lang.PersistentArrayMap\3437p\017\230\305\364\337\002\000\002L\000\005_metaq\000\176\000\001[\000\005arrayt\000\023[Ljava/lang/Object;xr\000\033clojure.lang.APersistentMap]\174/\003t r\173\002\000\002I\000\005_hashI\000\007_hasheqxp\000\000\000\000\000\000\000\000pur\000\023[Ljava.lang.Object;\220\316X\237\020s)l\002\000\000xp\000\000\000\006t\000\016getColumnCountsr\000%clojure.inspector$list_model$fn__8816H\252\320\325b\371!+\002\000\000xr\000\026clojure.lang.AFunction>\006p\234\236F\375\313\002\000\001L\000\021__methodImplCachet\000\036Lclojure/lang/MethodImplCache;xppt\000\013getRowCountsr\000%clojure.inspector$list_model$fn__8818-\037I\247\234/U\226\002\000\001L\000\005nrowst\000\022Ljava/lang/Object;xq\000\176\000\017ppt\000\012getValueAtsr\000%clojure.inspector$list_model$fn__8820\323\331\174ke\233\370\034\002\000\002L\000\011get_labelq\000\176\000\024L\000\011get_valueq\000\176\000\024xq\000\176\000\017ppp")

(deftest test-proxy-non-serializable
  (testing "That proxy classes refuse serialization and deserialization"
    ;; Serializable listed directly in interface list:
    (is (thrown? java.io.NotSerializableException
                 (-> (java.io.ByteArrayOutputStream.)
                     (java.io.ObjectOutputStream.)
                     (.writeObject (proxy [Object java.io.Serializable] [])))))
    ;; Serializable included via inheritence:
    (is (thrown? java.io.NotSerializableException
                 (-> (java.io.ByteArrayOutputStream.)
                     (java.io.ObjectOutputStream.)
                     (.writeObject (clojure.inspector/list-model nil)))))
    ;; Deserialization also prohibited:
    (is (thrown? java.io.NotSerializableException
                 (-> serialized-proxy (.getBytes "ISO-8859-1")
                     java.io.ByteArrayInputStream. java.io.ObjectInputStream.
                     .readObject)))))