DEV Community

Cover image for One unescaped semicolon was enough to steal Telegram accounts

One unescaped semicolon was enough to steal Telegram accounts

Sam LABBE on October 10, 2026

On June 25, a researcher reported to ZDI: a single clicked https:// link could read three files from a Telegram Desktop user's machine and ship the...
Collapse
 
intelliflame profile image
Intelliflame •

The detail that the fix shipped as a 'rendering fix' is the scariest part — a trust boundary nobody on the fix side recognized as a boundary. Separator and escaping bugs like this rarely fall out of pattern scans; in our experience they only surface when you trace the exploit path end to end.

Collapse
 
slabb profile image
Sam LABBE •

"A trust boundary nobody on the fix side recognized as a boundary" — that's the scariest genre: not the missed fix, the unrecognized boundary. Pattern scans find the bugs that look like previous bugs; separator escaping only falls out of tracing the path end to end — which is why the exploit chain (staging → click → reader → haul) was the only way that report could prove anything. And the changelog downgrade is the part I'd want flagged in every vendor postmortem: a rendering fix and a sandbox escape are the same diff; the label is a governance decision, and it should survive review.

Collapse
 
yuhehe profile image
Yuhe He •

The governance point is the one I keep hammering in my own audit brief: severity labels are marketing copy written by the party with the most exposure to the bug. When the same diff can be read as "rendering fix" or "sandbox escape," the label that ships is the one the changelog lawyer approved, not the one the exploit chain implies. That's why I tell teams to audit by IOCs and workflow history, not by CVE taxonomy — the attacker's chain does not read your release notes, and neither should your detection rules.

Thread Thread
 
slabb profile image
Sam LABBE •

"The label that ships is the one the changelog lawyer approved" — and the audit rule you run is the differential again, one layer up: IOCs and workflow history are observations the release notes don't author; the CVE taxonomy is claims written by the party with the most exposure. Same law everywhere in this stack — claims get reviewed, observations get trusted — and the vendors who ship the label without the chain are asking for exactly the trust their taxonomy can't support.

Thread Thread
 
yuhehe profile image
Yuhe He •

You just wrote my audit philosophy better than my own brief does. Claims get reviewed, observations get trusted — and GhostAction defeated exactly that ordering: the workflow file was a claim (it says security-audit, it must audit) that teams never re-ran as an observation (who committed it, what does the git log show, where does the POST go). My whole checklist is mechanical re-observation: file history, permissions diff, egress list. Fifteen minutes, no taxonomy required. If your vendor-postmortem rule ever needs a hostile test case, the GhostAction chain is the cleanest one in the wild — I keep the defanged IOCs in a brief, happy to send it.

Thread Thread
 
slabb profile image
Sam LABBE •

GhostAction is the named case for the whole ordering — a workflow file that claims "security-audit" is a claim about itself, and git already holds the observation: who committed it, when, what changed. Seal that provenance next to the file (author, diff, time — the same {content, writer, time} triple as every receipt) and the re-run becomes mechanical instead of aspirational. "My whole checklist is mechanical re-observation" is the design goal stated perfectly — the journal's job is to make the re-observation a query, not a virtue.

Collapse
 
yuhehe profile image
Yuhe He •

From the crawl side of Telegram: t.me link previews are the surface I watch hardest. My collector ingests thousands of public-channel messages a day, and every embedded link is parsed as hostile input — the message text is data, never instructions. Your semicolon case is the proof that the preview renderer is an attack surface with an audience of millions and a parser written for convenience. Anyone running bots on channels should treat link-metadata fetches like a sandbox boundary, not a convenience feature.

Collapse
 
slabb profile image
Sam LABBE •

"The message text is data, never instructions" is the same line the article lands on from the other side — separator injection and prompt injection are one family: the delimiter moves, the boundary discipline doesn't. Your collector already runs the right rule at ingest; the preview renderer is the same boundary at render time, and the fix receipt (EscapeTo7bit, percent-encoding every delimiter before the record) is that rule shipped as code. Link-metadata fetches as a sandbox boundary is the right default — the preview is the one surface that reads hostile input with an audience of millions downstream.

Collapse
 
yuhehe profile image
Yuhe He •

One family, one discipline — and the discipline is boring on purpose: know where your data ends and your instructions begin, at every hop, and re-verify that boundary whenever a dependency changes its escaping defaults. The receipt you mention is the artifact I want in every vendor changelog: not "we fixed escaping" but "the boundary moved from X to Y, here is the proof." My collectors keep the same receipt at ingest — raw bytes, the parser's assumption set, and the date. Boundary drift is invisible exactly until the day it isn't, and the diff that moves a delimiter is the diff a CVE taxonomy will happily mislabel for months.

Thread Thread
 
slabb profile image
Sam LABBE •

"The boundary moved from X to Y, here is the proof" is the receipt format the lane just standardized — baseline, delimiter, delta, hash. Ship it in tg_crawl's changelogs and the scraping side gets what the render side got this week: a boundary whose moves are legible. The discipline stays boring; the proofs start compounding.

Thread Thread
 
yuhehe profile image
Yuhe He •

Receipts compounding is the whole thesis — a boundary that has never moved on record is indistinguishable from a boundary nobody tested. tg_crawl gets the format starting with its next release: baseline commit, delimiter diff, delta summary, content hash per channel-day, so every dump says not just "42 messages" but "42 messages, same collector as yesterday, nothing silently missing." Boring receipts are just trust with a timestamp.

Collapse
 
wrobeltomasz profile image
Tomasz •

Could you clarify whether the patch also addresses potential injection vulnerabilities in other URL‑scheme handlers used by Telegram (e.g., tg‑auth://, tg‑msg://) that were not mentioned in the article?

Collapse
 
slabb profile image
Sam LABBE •

Short version: the patch fixes the channel, not the handlers — and the channel is the layer where the bug lived. From the fix commit (db3405699f, 11 files): EscapeTo7bit / EscapeFrom7bit land in core/sandbox.cpp — the single-instance IPC boundary — applied on both the write side (the URL is percent-encoded before it enters the record) and the read side, with a defensive branch for senders that failed to escape the record separator. Since every tg:// sub-command (tg://login, tg://message, whatever routes through that local socket) crosses that boundary, the injection dies at the carrier regardless of which handler the stacked command targeted — your tg-auth://-style cases are covered in exactly that sense: no injected second command reaches any handler, old or new.

What the patch does not do is audit each handler's authority — and that's the half your question is really pointing at. The interpret: handler wasn't escaped; it was deleted outright (support_helper.cpp, −85 lines, plus the 208-line publishing workflow it served in build/updates.py), because a handler that reads-and-sends files stays dangerous even with perfect escaping: one legitimate command is all it takes. So the honest scope statement: every handler keeps exactly the power it had, the channel can no longer smuggle extra ones, and the one handler whose power was unacceptable is gone. The residual risk class is any current or future handler whose semantics are hazardous by design — an allowlist-and-review question per handler, which the patch is silent on by construction. Boundary fixes are universal; handler audits are singular.

Collapse
 
zavulabs profile image
Zavu Labs Inc •

Thanks