Skip to content

[v5r0] Backport: secure JSON state encoding + DEncode depth check fix - #811

Merged
fstagni merged 2 commits into
DIRACGrid:rel-v5r0from
fstagni:fix_dencode_depth_v5r0
Oct 28, 2026
Merged

fstagni merged 2 commits into
DIRACGrid:rel-v5r0from
fstagni:fix_dencode_depth_v5r0

Conversation

@fstagni

@fstagni fstagni commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Backport of the UPHandler state-decoding security hardening to rel-v5r0, in two cherry-picked commits:

  1. fix: replace insecure DEncode deserialization with JSON (cherry-picked from integration) — rel-v5r0 was still calling the recursive DEncode.decode() directly on legacy data with no decompressed-size limit and no nesting-depth guard. This commit adds the JSON-first _safeDecode() with a 5 MB decompressed-size limit (zip-bomb protection) and _checkDEncodeDepth() (DoS protection against deeply nested DEncode structures).
  2. fix: DEncode depth check false positives on string payloads — corrects _checkDEncodeDepth, which counted every d/l/t/e byte as structure, including characters inside DEncode string payloads (s<len>:payload, arbitrary bytes). Valid legacy states whose payload contains enough container-like characters falsely raised DEncode data exceeds maximum nesting depth (50) / DEncode data has unbalanced nesting, producing HTTP 500 in web_listAppState (observed in production). The checker now walks the DEncode stream token by token, skipping string payloads and scalars, so only real containers contribute to depth. Genuinely deep nesting is still rejected; malformed data is rejected earlier with clearer messages.

web_listAppState also now logs and skips undecodable vars instead of failing the whole listing.

 _safeDecode(data)                       # rel-v5r0 before: no limits at all
+  size limit (5 MB)                     # zip-bomb protection
   json.loads(raw)                       # fails for legacy DEncode data
+  _checkDEncodeDepth(raw)               # token-aware depth guard
   DEncode.decode(raw)[0]

The resulting UPHandler.py is byte-identical to the version proposed for integration in #810.

Evidence

  • Before (v5r0): DEncode.decode() on arbitrary deeply nested legacy data → recursion near Python's recursion limit (~1000 frames) from a single authenticated request; and with the naive depth check (integration), DEncode.encode({"data": "d"*50}) (true nesting depth 2) raised ValueError: DEncode data exceeds maximum nesting depth (50) → HTTP 500.
  • After: same input passes; round-trip of all DEncode types (ints, longs, floats, bools, strings, lists, tuples, dicts, datetimes) passes against the real DIRAC.Core.Utilities.DEncode; b"d"*200 + b"e"*200 still rejected with the depth error; unbalanced and malformed streams rejected.

Merge Danger

Door: two-way

Blast Radius: UPHandler only

Affects only decoding (and, for newly saved states, JSON encoding) of application/desktop states in the user profile. Legacy states remain readable; newly saved states are pure JSON. Deployment note: sites running v5r0-era WebAppDIRAC that upgrade will decode both old (DEncode) and new (JSON) states.

DEncode uses a custom binary format (similar to bencode) with no
protection against deeply nested structures or untrusted input.

Changes:
- Use json.dumps() for encoding app state (web_saveAppState)
- Add _safeDecode() helper that tries JSON first, falls back to DEncode
  only for backward compatibility with legacy stored data
- Add decompressed size limit (5MB) to prevent zip bombs
- Add _checkDEncodeDepth() to prevent DoS via deeply nested structures
- Validate decoded types to prevent complex object injection
- Remove print() statement leaking user info in web_listPublicDesktopStates
- Fix double-encoding bug in web_changeView
- Remove unused 'random' import
_checkDEncodeDepth counted every d/l/t/e byte in the stream as
structure, including characters inside DEncode string payloads
(s<len>:payload), whose content is arbitrary bytes. Legacy stored
states whose payload contained enough container-like characters
falsely triggered 'DEncode data exceeds maximum nesting depth (50)'
and 'DEncode data has unbalanced nesting', turning valid data into
HTTP 500 errors in web_listAppState.

Walk the DEncode stream token by token instead, skipping string
payloads and scalar values, so that only real containers (d/l/t)
contribute to the nesting depth. Malformed data is now rejected
earlier with clearer messages, and genuinely deep nesting is still
rejected, keeping the DoS guard intact.

Also make web_listAppState skip vars that cannot be decoded instead
of failing the whole listing.
@fstagni
fstagni merged commit cf627c2 into DIRACGrid:rel-v5r0 Oct 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant