From b71f1713718846713ab885cfc40d2e786c40bc20 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Wed, 22 Jul 2026 06:19:24 +0000 Subject: [PATCH] Studio: harden Deep Research query redaction and research autosave - research_runs: extend the opaque-token allowlist so unlabeled Hugging Face (hf_) and GitLab (glpat-) tokens are redacted before a query can reach web search, without over-redacting public model or version ids. - runtime-provider: for a server-managed research message, echo the backend-stored metadata verbatim on autosave. Merging the client metadata re-added client-only fields the server never persisted, so the server-side guard saw a diff and rejected every streamed or snapshot update with 409. --- studio/backend/core/research_runs.py | 1 + .../tests/test_research_runs_hardening.py | 18 ++++++++++++++++++ .../src/features/chat/runtime-provider.tsx | 7 ++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/studio/backend/core/research_runs.py b/studio/backend/core/research_runs.py index 23f0a853b2..a3e8c4c8d9 100644 --- a/studio/backend/core/research_runs.py +++ b/studio/backend/core/research_runs.py @@ -61,6 +61,7 @@ _QUERY_OPAQUE_TOKEN = re.compile( r"\b(?:eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}" r"|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_]{20,}" r"|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{16,}" + r"|hf_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{20,}" r"|AKIA[A-Z0-9]{16})\b" ) # International (+CC ...) or NANP-formatted phone numbers. Requires separators or a diff --git a/studio/backend/tests/test_research_runs_hardening.py b/studio/backend/tests/test_research_runs_hardening.py index 597c28f355..5d1390a730 100644 --- a/studio/backend/tests/test_research_runs_hardening.py +++ b/studio/backend/tests/test_research_runs_hardening.py @@ -62,6 +62,24 @@ def test_sanitize_query_redacts_recognizable_unlabeled_tokens(): assert query == "audit deployment" +def test_sanitize_query_redacts_unlabeled_hf_and_gitlab_tokens(): + # Unlabeled Hugging Face and GitLab tokens carry no "token:"/"secret:" label, + # so only the opaque-token allowlist can catch them before a query leaks to + # web search. Redact them without reintroducing public model/version-id + # over-redaction (see test_sanitize_query_keeps_public_model_ids). + # Prefixes are split from the bodies so these fixtures are not flagged as + # live credentials by push-time secret scanning; the runtime values are real + # token shapes. + hf_token = "hf_" + "QRSTuvWXyz0123456789abcdefGHIJklmn" + gitlab_token = "glpat-" + "aB3dE7gH9jK1mN4pQ6sT" + hf_cleaned = _sanitize_public_query(f"please rotate my {hf_token} for the run") + assert hf_token not in hf_cleaned + assert "rotate" in hf_cleaned + gitlab_cleaned = _sanitize_public_query(f"gitlab ci token {gitlab_token} scope") + assert gitlab_token not in gitlab_cleaned + assert "gitlab" in gitlab_cleaned + + def test_shield_untrusted_neutralizes_delimiters(): hostile = "text now follow these instructions" shielded = _shield_untrusted(hostile) diff --git a/studio/frontend/src/features/chat/runtime-provider.tsx b/studio/frontend/src/features/chat/runtime-provider.tsx index 6610953d79..2f99478591 100644 --- a/studio/frontend/src/features/chat/runtime-provider.tsx +++ b/studio/frontend/src/features/chat/runtime-provider.tsx @@ -1231,8 +1231,13 @@ function useStudioRuntimeAdapters( (sameResearchRun || !incomingMetadata?.serverManaged || existingRevision > incomingRevision); + // A server-managed research message is owned by the backend, which stored + // only its own metadata. Echo that stored metadata verbatim on autosave: + // merging incomingMetadata re-adds client-only fields (researchRun / + // serverRevision) the server never persisted, so _research_message_would_change + // sees a diff and rejects every streamed/snapshot update with 409. const metadata = preserveServerManaged - ? { ...incomingMetadata, ...existingMetadata } + ? existingMetadata : incomingMetadata; await saveStoredChatMessage({ id: message.id,