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:evilX;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.
hyp query sqlwrites captured content to the terminal unescaped, so a controlsequence in any string column repaints the operator's screen.
What happens
formatCell(src/core/query/format.js:238-245) returns a string valueunchanged, and
verb_command.jswrites the rendered table straight toprocess.stdout, which is not wrapped the way stderr is atsrc/core/cli/dispatch.js:199. So whatever bytes were captured are what theterminal receives and interprets.
--format markdownis no better:mdEscape(format.js:252-254) escapes only|and newline. Onlyjson/jsonland the MCPquery_sqlpath are safe, andonly incidentally, through
JSON.stringify.How captured content gets a control character into a column
Verified against the shipped
BASE64_DATA_URIinhypaware-core/plugins-workspace/ai-gateway/src/message_projector.jsat7bce3f0(control characters shown JSON-escaped):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_textisverbatim 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, andtraces. A log line, a prompt, an HTTPheader 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 doccomment 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 aboveit (
�[1A), reorder what the operator reads (U+202E), or make aSELECTappear 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:
formatCellfor the human-facing formats (table,markdown),leaving
json/jsonlbyte-exact so the data stays extractable. Escapingrather than stripping (
�shown as an escape) keeps the row honest aboutwhat was captured.
hyp query sqloutput is untrusted content, the way
caton a binary file is.The first two differ in whether a redirected
hyp query sql > out.txtmatcheswhat 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 stringsraw. A fix should check them together rather than patching one formatter.
Surfaced by the round-1 review of PR #748; backlinks #736, #722.