Skip to content

hyp query sql renders captured strings raw, so a control sequence in any column reaches the terminal #752

Description

@philcunliffe

hyp query sql writes captured content to the terminal unescaped, so a control
sequence in any string column repaints the operator's screen.

What happens

formatCell (src/core/query/format.js:238-245) returns a string value
unchanged, and verb_command.js writes the rendered table straight to
process.stdout, which is not wrapped the way stderr is at
src/core/cli/dispatch.js:199. So whatever bytes were captured are what the
terminal receives and interprets.

--format markdown is no better: mdEscape (format.js:252-254) escapes only
| and newline. Only json / jsonl and the MCP query_sql path are safe, and
only incidentally, through JSON.stringify.

How captured content gets a control character into a column

Verified against the shipped BASE64_DATA_URI in
hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js at
7bce3f0 (control characters shown JSON-escaped):

regex: /data:([^\s,]{0,255}?);base64,[A-Za-z0-9+/=_-]+/g
esc      ADMITTED  "row: data:�[31mPWNED�[0m;base64,<stripped> end"
nul      ADMITTED  "data:evilX;base64,<stripped>"
bs       ADMITTED  "data:evil\bX;base64,<stripped>"
rlo      ADMITTED  "data:evil‮X;base64,<stripped>"
dquote   ADMITTED  "data:a\"b;base64,<stripped>"
newline  UNTOUCHED (no match)
tab      UNTOUCHED (no match)
comma    UNTOUCHED (no match)

That path is only the example that surfaced this (see PR #748's review). It is
not the cause and fixing it there would not fix this: content_text is
verbatim captured prose
, so an ESC can arrive in it without any regex being
involved at all, and the same is true of every other captured string column
across ai_gateway_messages, logs, and traces. A log line, a prompt, an HTTP
header value, a filename: anything a captured process emitted.

Why this is worth an issue rather than a shrug

The repo already treats this exact threat as real elsewhere. sanitizeLabel
(src/core/util/json_util.js:92-101) strips unsafe characters, and its doc
comment names terminal repainting as the reason. #680/#681 exist to make one
warning's upstream names go through it "like every other status-file label". The
query plane is the one surface where far more attacker-influenced text reaches a
terminal, and it is the surface with no sanitization at all.

Concretely, a row can hide its own content (�[8m), overwrite the row above
it (�[1A), reorder what the operator reads (U+202E), or make a SELECT
appear to return something it did not. An operator triaging captured traffic is
exactly the person who should not be reading a doctored table.

Deliberately not proposing the fix

Which way this should go is a real decision and I do not want to prejudge it:

  • Sanitize in formatCell for the human-facing formats (table, markdown),
    leaving json/jsonl byte-exact so the data stays extractable. Escaping
    rather than stripping ( shown as an escape) keeps the row honest about
    what was captured.
  • Or sanitize only when stdout is a TTY, so pipelines keep exact bytes.
  • Or treat it as out of scope for the CLI and document that hyp query sql
    output is untrusted content, the way cat on a binary file is.

The first two differ in whether a redirected hyp query sql > out.txt matches
what the terminal showed, which is the kind of thing worth deciding once and
writing down.

Not claimed here

I did not test against a real terminal emulator, and I did not survey which other
CLI surfaces (hyp status, hyp graph, the walkthrough) render captured strings
raw. A fix should check them together rather than patching one formatter.

Surfaced by the round-1 review of PR #748; backlinks #736, #722.

Metadata

Metadata

Assignees

No one assigned

    Labels

    neutral:fixDelegate this issue to neutral for an autonomous fix attempt (reproduce -> fix -> PR)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions