fix(share): reject dict-valued text-blocks and dict titles (re-gate edge)
The re-gate found the always-on sanitizer still had two str()-on-dict paths: a list text-block whose 'text' is itself a dict (via /api/session/import), and a dict-valued session title. Both are now type-checked to str; a dict title falls back to 'Untitled'. Regression cases added. Co-authored-by: MicoRobot.
This commit is contained in:
1 parent
75153f5b79
commit
a1312acc5d
2 files changed
+34
-2
No files matched your search
+11
-2
@@ -77,7 +77,12 @@ def _share_message_text(message: dict) -> str:
|
||||
# never stringify them into the public snapshot.
|
||||
continue
|
||||
if item.get("type") == "text":
|
||||
parts.append(str(item.get("text") or ""))
|
||||
# Only append genuine string text — a dict-valued "text" (possible
|
||||
# via /api/session/import) must NOT be str()'d into the public
|
||||
# snapshot (that would publish structured/tool payload verbatim).
|
||||
_t = item.get("text")
|
||||
if isinstance(_t, str):
|
||||
parts.append(_t)
|
||||
return "".join(parts).strip()
|
||||
if isinstance(content, str):
|
||||
return content.strip()
|
||||
@@ -176,7 +181,11 @@ def build_share_snapshot(session) -> dict:
|
||||
safe_messages.append(sanitized)
|
||||
if not safe_messages:
|
||||
raise ValueError("This conversation has no shareable messages yet.")
|
||||
title = _force_redact_credentials(str(safe_session.get("title") or "Untitled"))
|
||||
# Only accept a genuine string title — a dict-valued title (possible via
|
||||
# /api/session/import) must not be str()'d into the public snapshot.
|
||||
_raw_title = safe_session.get("title")
|
||||
_raw_title = _raw_title if isinstance(_raw_title, str) else "Untitled"
|
||||
title = _force_redact_credentials(_raw_title or "Untitled")
|
||||
title = _redact_share_paths(title, redact_paths) or "Untitled"
|
||||
return {
|
||||
"title": title,
|
||||
|
||||
@@ -105,6 +105,8 @@ def test_share_snapshot_redaction_is_always_on_regardless_of_setting():
|
||||
{"role": "system", "content": "internal system prompt stays private"},
|
||||
{"role": "user", "content": "key sk-SECRET1234567890abcdef path /very/private/workspace/x.py"},
|
||||
{"role": "assistant", "content": {"tool": "terminal", "output": "raw structured tool payload"}},
|
||||
# dict-valued "text" inside a text block (possible via /api/session/import)
|
||||
{"role": "assistant", "content": [{"type": "text", "text": {"tool": "terminal", "output": "STRUCTURED_SECRET_IN_TEXTBLOCK"}}]},
|
||||
{"role": "tool", "content": "raw tool output"},
|
||||
{"role": "assistant", "content": "Here is the answer."},
|
||||
]
|
||||
@@ -118,10 +120,31 @@ def test_share_snapshot_redaction_is_always_on_regardless_of_setting():
|
||||
assert "/very/private/workspace" not in blob
|
||||
# structured/dict content never stringified into the public payload
|
||||
assert "raw structured tool payload" not in blob
|
||||
assert "STRUCTURED_SECRET_IN_TEXTBLOCK" not in blob
|
||||
# system + tool roles excluded
|
||||
roles = [m["role"] for m in snap["messages"]]
|
||||
assert "system" not in roles and "tool" not in roles
|
||||
# the real answer is preserved
|
||||
assert any("Here is the answer" in m["content"] for m in snap["messages"])
|
||||
|
||||
finally:
|
||||
config.load_settings = _orig
|
||||
|
||||
|
||||
def test_share_snapshot_rejects_dict_valued_title():
|
||||
# A dict-valued title (possible via /api/session/import) must not be
|
||||
# stringified into the public snapshot — fall back to "Untitled".
|
||||
import os, tempfile
|
||||
os.environ.setdefault("HERMES_WEBUI_STATE_DIR", tempfile.mkdtemp())
|
||||
import api.shares as shares
|
||||
|
||||
class _S:
|
||||
pass
|
||||
s = _S()
|
||||
s.title = {"leak": "STRUCTURED_TITLE_SECRET"}
|
||||
s.workspace = ""
|
||||
s.messages = [{"role": "user", "content": "hello"}, {"role": "assistant", "content": "hi"}]
|
||||
snap = shares.build_share_snapshot(s)
|
||||
import json
|
||||
assert "STRUCTURED_TITLE_SECRET" not in json.dumps(snap)
|
||||
assert snap["title"] == "Untitled"
|
||||
Reference in new issue
Block a user