Serializable: add allow-and-record-any-serializable-class-unsafe

A convenience for folks upgrading from older versions of Nippy
still vulnerable to #130.
This commit is contained in:
Peter Taoussanis 2020-09-12 09:11:02 +02:00
parent d7229f8665
commit 478160ed85
2 changed files with 118 additions and 45 deletions

View file

@ -310,6 +310,7 @@
"clojure.lang.ExceptionInfo" "clojure.lang.ExceptionInfo"
"clojure.lang.ArityException"}) "clojure.lang.ArityException"})
(defn- allow-and-record? [s] (= s "allow-and-record"))
(defn- split-class-names>set [s] (when (string? s) (if (= s "") #{} (set (mapv str/trim (str/split s #"[,:]")))))) (defn- split-class-names>set [s] (when (string? s) (if (= s "") #{} (set (mapv str/trim (str/split s #"[,:]"))))))
(comment (comment
(split-class-names>set "") (split-class-names>set "")
@ -331,14 +332,14 @@
(when-let [s (or (when-let [s (or
(enc/get-sys-val (get-in ids [action :base :prop]) (get-in ids [action :base :env])) (enc/get-sys-val (get-in ids [action :base :prop]) (get-in ids [action :base :env]))
(enc/get-sys-val (get-in ids [:legacy :base :prop]) (get-in ids [:legacy :base :env])))] (enc/get-sys-val (get-in ids [:legacy :base :prop]) (get-in ids [:legacy :base :env])))]
(split-class-names>set s)) (if (allow-and-record? s) s (split-class-names>set s)))
default) default)
allowlist-add allowlist-add
(when-let [s (or (when-let [s (or
(enc/get-sys-val (get-in ids [action :add :prop]) (get-in ids [action :add :env])) (enc/get-sys-val (get-in ids [action :add :prop]) (get-in ids [action :add :env]))
(enc/get-sys-val (get-in ids [:legacy :add :prop]) (get-in ids [:legacy :add :env])))] (enc/get-sys-val (get-in ids [:legacy :add :prop]) (get-in ids [:legacy :add :env])))]
(split-class-names>set s))] (if (allow-and-record? s) s (split-class-names>set s)))]
(if (and allowlist-base allowlist-add) (if (and allowlist-base allowlist-add)
(into (enc/have set? allowlist-base) allowlist-add) (into (enc/have set? allowlist-base) allowlist-add)
@ -346,11 +347,11 @@
(let [doc (let [doc
"Used when attempting to <freeze/thaw> an object that: "Used when attempting to <freeze/thaw> an object that:
- Does not implement Nippy's Freezable protocol. - Does NOT implement Nippy's Freezable protocol.
- Does implement Java's Serializable interface. - DOES implement Java's Serializable interface.
In this case, Java's Serializable interface will be permitted iff In this case, the allowlist will be checked to see if Java's
`(<allowlist> <class-name>)` predicate call returns true. Serializable interface may be used.
This is a security measure to prevent possible Remote Code Execution This is a security measure to prevent possible Remote Code Execution
(RCE) when thawing malicious payloads. See [1] for details. (RCE) when thawing malicious payloads. See [1] for details.
@ -370,9 +371,10 @@
- `*freeze-serializable-allowlist*` ; Checked when freezing - `*freeze-serializable-allowlist*` ; Checked when freezing
- `*thaw-serializable-allowlist*` ; Checked when thawing - `*thaw-serializable-allowlist*` ; Checked when thawing
Example values: Example allowlist values:
- `(fn allow-class? [class-name] true)` ; Arbitrary predicate fn - `(fn allow-class? [class-name] true)` ; Arbitrary predicate fn
- `#{\"java.lang.Throwable\", \"clojure.lang.*\"}` ; Set of class-names - `#{\"java.lang.Throwable\", \"clojure.lang.*\"}` ; Set of class-names
- `\"allow-and-record\"` ; Special value, see [2]
Note that class-names in sets may contain \"*\" wildcards. Note that class-names in sets may contain \"*\" wildcards.
@ -396,55 +398,112 @@
- The \"base\" property/var to replace Nippy's default allowlists. - The \"base\" property/var to replace Nippy's default allowlists.
- The \"add\" property/var to add to Nippy's default allowlists. - The \"add\" property/var to add to Nippy's default allowlists.
See also `taoensso.encore/compile-str-filter`, a util to help The special `\"allow-and-record\"` value is also possible, see [2].
easily build more advanced predicate functions.
Upgrading from an older version of Nippy and unsure whether you've been Upgrading from an older version of Nippy and unsure whether you've been
using Nippy's Serializable support? Here's a snippet to ALLOW and RECORD using Nippy's Serializable support, or which classes to allow? See [2].
any class requesting Nippy's Serializable fallback:
;; Deref for set of all class names that made use of Nippy's Serializable support:
(defonce observed-serializables_ (atom #{}))
(let [f (fn allow-class? [class-name]
(swap! observed-serializables_ conj class-name) ; Record class name
true ; Allow any class
)]
(alter-var-root #'*freeze-serializable-allowlist* (fn [_] f))
(alter-var-root #'*thaw-serializable-allowlist* (fn [_] f)))
See also `taoensso.encore/compile-str-filter` for a util to help easily
build more advanced predicate functions.
Thanks to Timo Mihaljov (@solita-timo-mihaljov) for an excellent report Thanks to Timo Mihaljov (@solita-timo-mihaljov) for an excellent report
identifying this vulnerability. identifying this vulnerability.
[1] https://github.com/ptaoussanis/nippy/issues/130"] [1] https://github.com/ptaoussanis/nippy/issues/130
[2] See `allow-and-record-any-serializable-class-unsafe`."]
(enc/defonce ^{:dynamic true :doc doc} *freeze-serializable-allowlist* (init-allowlist :freeze default-freeze-serializable-allowlist)) (enc/defonce ^{:dynamic true :doc doc} *freeze-serializable-allowlist* (init-allowlist :freeze default-freeze-serializable-allowlist))
(enc/defonce ^{:dynamic true :doc doc} *thaw-serializable-allowlist* (init-allowlist :thaw default-thaw-serializable-allowlist))) (enc/defonce ^{:dynamic true :doc doc} *thaw-serializable-allowlist* (init-allowlist :thaw default-thaw-serializable-allowlist)))
(comment (let [nmax 1000
;; Deref for set of all class names that made use of Nippy's Serializable support: gc-rate (/ 1.0 16000)
(defonce observed-serializables_ (atom #{})) state_ (atom {}) ; {<class-name> <frequency>}
lock_ (atom nil) ; ?promise
trim (fn [nmax state]
(persistent!
(enc/reduce-top nmax val enc/rcompare conj!
(transient {}) state)))]
(let [f (fn allow-class? [class-name] ;; Note: trim strategy isn't perfect: it can be tough for new
(swap! observed-serializables_ conj class-name) ; Record class name ;; classes to break into the top set since frequencies are being
true ; Allow any class ;; reset only for classes outside the top set.
)] ;;
;; In practice this is probably good enough since the main objective
;; is to discard one-off anonymous classes to protect state from
;; endlessly growing. Also `gc-rate` allows state to temporarily grow
;; significantly beyond `nmax` size, which helps to give new classes
;; some chance to accumulate a competitive frequency before next GC.
(alter-var-root #'*freeze-serializable-allowlist* (fn [_] f)) (defn- ^{:-state_ state_} ; Undocumented
(alter-var-root #'*thaw-serializable-allowlist* (fn [_] f))) allow-and-record-any-serializable-class-unsafe
"A predicate (fn allow-class? [class-name]) fn that can be assigned
to `*freeze-serializable-allowlist*` and/or
`*thaw-serializable-allowlist*` that:
- Will allow ANY class to use Nippy's Serializable support (unsafe).
- And will record {<class-name> <frequency-allowed>} for the <=1000
classes that ~most frequently made use of this support.
`get-recorded-serializable-classes` returns the recorded state.
This predicate is provided as a convenience for users upgrading from
previous versions of Nippy that allowed the use of Serializable for all
classes by default.
While transitioning from an unsafe->safe configuration, you can use
this predicate (unsafe) to record information about which classes have
been using Nippy's Serializable support in your environment.
Once some time has passed, you can check the recorded state. If you're
satisfied that all recorded classes are safely Serializable, you can
then merge the recorded classes into Nippy's default allowlist/s, e.g.:
(comment @observed-serializables_) ; Call/log after some time
(comment
;; If you're satisfied that the recorded classes are safe, you can merge them
;; into Nippy's default allowlist:
(alter-var-root #'thaw-serializable-allowlist* (alter-var-root #'thaw-serializable-allowlist*
(fn [_] (into default-thaw-serializable-allowlist observed-serializables_))))) (fn [_] (into default-thaw-serializable-allowlist
(keys (get-recorded-serializable-classes)))))"
[class-name]
(when-let [p @lock_] @p)
(let [n (count
(swap! state_
(fn [m] (assoc m class-name
(inc (long (or (get m class-name) 0)))))))]
;; Garbage collection (GC): may be serializing anonymous classes, etc.
;; so input domain could be infinite
(when (> n nmax) ; Too many classes recorded, uncommon
(when (< (java.lang.Math/random) gc-rate) ; Amortize GC cost
(let [p (promise)]
(when (compare-and-set! lock_ nil p) ; Acquired gc lock
(try
(do (reset! state_ (trim nmax @state_))) ; GC state
(finally (reset! lock_ nil) (deliver p nil)))))))
n))
(defn get-recorded-serializable-classes
"Returns {<class-name> <frequency>} of the <=1000 classes that ~most
frequently made use of Nippy's Serializable support via
`allow-and-record-any-serializable-class-unsafe`.
See that function's docstring for more info."
[] (trim nmax @state_)))
(comment
(count (get-recorded-serializable-classes))
(enc/reduce-n
(fn [_ n] (allow-and-record-any-serializable-class-unsafe (str n)))
nil 0 1e5))
(let [fn? fn? (let [fn? fn?
compile (enc/fmemoize (fn [x] (enc/compile-str-filter x))) compile
(enc/fmemoize
(fn [x]
(if (allow-and-record? x)
allow-and-record-any-serializable-class-unsafe
(enc/compile-str-filter x))))
conform?* (fn [x cn] ((compile x) cn)) ; Uncached because input domain possibly infinite conform?* (fn [x cn] ((compile x) cn)) ; Uncached because input domain possibly infinite
conform? conform?
(fn [x cn] (fn [x cn]

View file

@ -248,8 +248,10 @@
;;;; Serializable ;;;; Serializable
(def ^:private sem (java.util.concurrent.Semaphore. 1)) (do
(defn- sem? [x] (instance? java.util.concurrent.Semaphore x)) (def ^:private semcn "java.util.concurrent.Semaphore")
(def ^:private sem (java.util.concurrent.Semaphore. 1))
(defn- sem? [x] (instance? java.util.concurrent.Semaphore x)))
(deftest _serializable (deftest _serializable
(is (= nippy/*thaw-serializable-allowlist* #{"base.1" "base.2" "add.1" "add.2"}) (is (= nippy/*thaw-serializable-allowlist* #{"base.1" "base.2" "add.1" "add.2"})
@ -260,8 +262,8 @@
(is (sem? (is (sem?
(nippy/thaw (nippy/thaw
(nippy/freeze sem {:serializable-allowlist #{"java.util.concurrent.Semaphore"}}) (nippy/freeze sem {:serializable-allowlist #{semcn}})
{:serializable-allowlist #{"java.util.concurrent.Semaphore"}})) {:serializable-allowlist #{semcn}}))
"Can freeze and thaw Serializable objects if approved by allowlist") "Can freeze and thaw Serializable objects if approved by allowlist")
@ -283,7 +285,19 @@
(is (sem? (nippy/read-quarantined-serializable-object-unsafe! (is (sem? (nippy/read-quarantined-serializable-object-unsafe!
(nippy/thaw (nippy/freeze thawed)))) (nippy/thaw (nippy/freeze thawed))))
"Quarantined Serializable objects are themselves safely transportable.")) ) "Quarantined Serializable objects are themselves safely transportable."))
(let [obj
(nippy/thaw
(nippy/freeze sem)
{:serializable-allowlist "allow-and-record"})]
(is (sem? obj)
"Special \"allow-and-record\" allowlist permits any class")
(is
(contains? (nippy/get-recorded-serializable-classes) semcn)
"Special \"allow-and-record\" allowlist records classes")))
;;;; Metadata ;;;; Metadata