fix: settings card export -- round 3 found the DoS bound in a new place
extensive_testing round 3 found round 2's fix was itself incomplete: it
bounded row count and per-key value length, but not the JOINED row string
_rows_from_list builds from a dict with many keys. A funpack_list entry
like {"k1": "x"*400, ..., "k150": "x"*400} produced one row whose value had
no length cap of its own -- row count and per-key length both stayed under
every existing budget while the report JSON embedded in the PNG's tEXt
chunk (which scales 1:1 with it) reached 123MB, reproducing round 2's exact
number through a different attack shape, with zero truncation declared
anywhere on the resulting card.
Fixed by running the joined row string back through _short() (closing the
length gap) and capping how many keys get processed per row at 50 before
that join even happens (closing a related CPU-amplification gap: an absurd
key count still cost one f-string+join per key before any length cap could
apply). Added a test reproducing the exact many-key-row shape and asserting
on render_png's actual wall-clock time and output size, not just collect()'s
data.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7005de86e5
commit
ffb40d271f
+14
-2
@@ -64,6 +64,7 @@ _NOISE_INPUTS = {"control_after_generate"}
|
||||
# so this is a real limit worth declaring rather than a corner cut short.
|
||||
_MAX_VALUE_CHARS = 400
|
||||
_MAX_ROWS = 2000
|
||||
_MAX_KEYS_PER_LIST_ROW = 50
|
||||
|
||||
|
||||
def _short(value):
|
||||
@@ -94,8 +95,19 @@ def _rows_from_list(value):
|
||||
return None
|
||||
out = []
|
||||
for i, row in enumerate(rows[:_MAX_ROWS], 1):
|
||||
parts = [f"{k}={_short(v)}" for k, v in row.items()]
|
||||
out.append((f"[{i}]", " ".join(parts) if parts else "—"))
|
||||
# extensive_testing round 3: _short() on each KEY's value bounded
|
||||
# nothing about the JOINED row -- a dict with many keys produced one
|
||||
# row whose string had no cap of its own, so row count and per-key
|
||||
# length both stayed under budget while a single row's value (and
|
||||
# the report JSON embedded in the PNG's tEXt chunk, which scales
|
||||
# 1:1 with it) ballooned past 100MB. Also bound how many keys get
|
||||
# processed at all: a row with an absurd key count still costs one
|
||||
# f-string+join per key before any length cap ever applies.
|
||||
items = list(row.items())
|
||||
parts = [f"{k}={_short(v)}" for k, v in items[:_MAX_KEYS_PER_LIST_ROW]]
|
||||
if len(items) > _MAX_KEYS_PER_LIST_ROW:
|
||||
parts.append(f"…(+{len(items) - _MAX_KEYS_PER_LIST_ROW} more keys)")
|
||||
out.append((f"[{i}]", _short(" ".join(parts)) if parts else "—"))
|
||||
if len(rows) > _MAX_ROWS:
|
||||
out.append(("", f"… {len(rows) - _MAX_ROWS} more rows omitted"))
|
||||
return out # [] for an empty list; the caller collapses that onto one line
|
||||
|
||||
@@ -184,6 +184,27 @@ def test_render_png_stays_fast_even_when_both_caps_are_individually_honored():
|
||||
assert len(png) < 5_000_000
|
||||
|
||||
|
||||
def test_a_funpack_list_row_with_many_keys_does_not_blow_up_the_embed():
|
||||
"""extensive_testing round 3: _short() bounded each KEY's value but not
|
||||
the JOINED row -- a dict with 150 keys x 400-char values produced one
|
||||
row string with no cap of its own. Row count and per-key length both
|
||||
stayed under budget while the report JSON embedded in the PNG's tEXt
|
||||
chunk (which scales with it) reached 123MB, with zero truncation
|
||||
declared anywhere on the card."""
|
||||
import json
|
||||
import time
|
||||
entry = {f"k{i}": "x" * sc._MAX_VALUE_CHARS for i in range(150)}
|
||||
big_list = json.dumps([entry for _ in range(2000)])
|
||||
rep = sc.collect([{"id": "s", "node": "X", "inputs": {"list_1": big_list}}], HOST)
|
||||
text = " ".join(v for _, v in rep["sections"][0]["rows"])
|
||||
assert len(text) < 2_000_000
|
||||
start = time.monotonic()
|
||||
png = sc.render_png(rep, "dark")
|
||||
elapsed = time.monotonic() - start
|
||||
assert elapsed < 5.0
|
||||
assert len(png) < 5_000_000
|
||||
|
||||
|
||||
def test_render_png_stays_fast_when_one_slot_has_many_large_lists():
|
||||
"""The Finding B composition gap, carried all the way through to the
|
||||
actual render (67.8s / 123MB before the fix)."""
|
||||
|
||||
Reference in New Issue
Block a user