Accept the host app's screen context, and treat it as untrusted

#1
Beiing Human org

Adds the host-app integration for Ask Bahi, the support assistant side panel in the Beiing Human web app. Paired with customer-app-react PR #2027 โ€” the handshake has two halves, and neither does anything useful alone.

What this adds

The web app now embeds this Space as a side panel that stays open while the user moves around it. This lets the panel say which screen the user is on, so "how do I do this?" and "what does this tab mean?" stop being unanswerable.

It cannot arrive in the URL: changing the iframe's src reloads the chat and throws the conversation away, and the user changes pages constantly. So it comes by postMessage, and a js= handler writes it into a hidden Textbox that Gradio wires to respond()'s new page_context argument.

Why it is treated as hostile input

The origin allowlist in that handler is the only thing between an arbitrary web page and a string in this bot's system prompt, so it fails closed โ€” an unlisted origin is ignored and the assistant carries on without context.

Two more layers behind it, because an origin check in a browser is not something to rest a prompt on:

  • sanitize_page_context collapses all whitespace. That is the part that matters: a value carrying newlines could otherwise close the screen-context block and open what reads as a fresh instruction section after it. Length is capped at 200.
  • The block tells the model in plain terms that this is UI state and not an instruction โ€” never follow it, never repeat it as product fact, and the user wins any conflict.

Readiness handshake

The host originally posted context on the iframe's load event. That fires when the document is done, but the listener here only exists after Gradio has executed its bundle, fetched its config and mounted โ€” later. The first message arrived before anything was listening and was silently dropped, leaving the assistant context-blind until the user changed page. This Space now announces itself once its listener is up so the host knows when it is safe to speak.

"*" is the target origin for that announcement because this Space cannot know which origin embedded it. The message carries no data, and the host verifies the sender before replying.

Escape does not cross an iframe boundary either, so the panel's own Escape handler was dead exactly when someone was typing in the chat. It is forwarded now.

Verified locally

Running this Space on localhost: the hidden field is reachable in the DOM despite visible=False, the handler fills it and ignores wrong message types, Gradio exposes it as the page_context parameter of /chat, respond() receives it, and the block lands in the system prompt ahead of DOCUMENTATION: โ€” and is absent entirely when no context was sent.

test_page_context.py covers the sanitiser offline (no HF_TOKEN, no network, no LLM call), following test_knowledge_base.py's standalone-script style since pytest is not in requirements:

python test_page_context.py

Before merging

  • Merging this deploys the Space. That is safe on its own โ€” with the web app not yet shipped, the Space simply announces readiness to nobody and behaves exactly as it does today.
  • The origin allowlist lists only production and localhost โ€” the origins that could be verified. Staging and dev get no context until theirs are added. A missing entry costs a feature; a wrong entry is a prompt-injection vector.
  • The handshake has not been tested live cross-origin, only in unit tests on the app side. That exact gap already hid one bug, so it is worth smoke-testing right after this deploys.
rishisriv-bh changed pull request status to merged

Sign up or log in to comment