From 0481408531e5447ffc2035b914099c0535f73b59 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 5 Aug 2026 17:41:59 +0200 Subject: [PATCH] :bug: Add recursion depth limit to Fressian reader (#11020) Bound read depth at 128 levels to prevent StackOverflowError from crafted deeply-nested payloads. All recursive read handlers go through read-object!, so a single depth check covers all paths. AI-assisted-by: mimo-v2.5-pro --- common/src/app/common/fressian.clj | 13 ++++++++++++- common/test/common_tests/fressian_test.clj | 18 +++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/common/src/app/common/fressian.clj b/common/src/app/common/fressian.clj index b16d233b42..3de4cc54a7 100644 --- a/common/src/app/common/fressian.clj +++ b/common/src/app/common/fressian.clj @@ -31,6 +31,11 @@ ([^String s, ^String encoding] (.getBytes s encoding))) +;; --- DEPTH TRACKING + +(def ^:dynamic *read-depth* 0) +(def ^:const max-read-depth 128) + ;; --- LOW LEVEL FRESSIAN API (defn write-object! @@ -41,7 +46,13 @@ (defn read-object! [^Reader r] - (.readObject r)) + (when (>= *read-depth* max-read-depth) + (throw (ex-info "maximum Fressian read depth exceeded" + {:type :validation + :code :max-read-depth-reached + :hint "maximum Fressian read depth exceeded"}))) + (binding [*read-depth* (inc *read-depth*)] + (.readObject r))) (defn write-tag! ([^Writer w ^String n] diff --git a/common/test/common_tests/fressian_test.clj b/common/test/common_tests/fressian_test.clj index 9af54464a5..3eda0f34d4 100644 --- a/common/test/common_tests/fressian_test.clj +++ b/common/test/common_tests/fressian_test.clj @@ -21,7 +21,8 @@ (:import java.time.Instant java.time.OffsetDateTime - java.time.ZoneOffset)) + java.time.ZoneOffset + java.util.UUID)) ;; --------------------------------------------------------------------------- ;; Helpers @@ -524,3 +525,18 @@ (t/is (d/ordered-map? rt)) (t/is (= om rt)) (t/is (= (keys om) (keys rt))))) + +(t/deftest decode-rejects-excessive-recursion-depth + ;; N2-01: deeply nested structures must be rejected before stack overflow + (let [depth (+ fres/max-read-depth 50) + data (reduce (fn [acc _i] [acc]) + :leaf + (range depth)) + encoded (fres/encode data)] + (try + (fres/decode encoded) + (t/is false "expected exception for excessive recursion depth") + (catch clojure.lang.ExceptionInfo e + (let [d (ex-data e)] + (t/is (= :validation (:type d))) + (t/is (= :max-read-depth-reached (:code d))))))))