2026-10-10 · 9 min read

The Semicolon Is the Vulnerability: When Your Format Lets the Data Become the Command

Flat isometric illustration of a message traveling between two process nodes on a dark background, splitting into two messages at a narrow gap in an indigo channel, with one message glowing green.

Someone adds you to a Telegram group. A link shows up in the chat. You click it, and the Telegram account on your laptop is no longer only yours.

No malware, no scary dialog, no admin rights. Just a semicolon that nobody escaped. That is CVE-2026-107181, disclosed by security researcher beaksec and fixed in Telegram Desktop 7.2.9. It is rated 8.1 High: remote arbitrary file read that leads to full account takeover.

Telegram is the headline, not the lesson. The bug underneath is the most reusable mistake in software: you built a format whose separator character is allowed to appear inside the values. You have probably shipped it. It is in your logs, your exports, your agent tool calls, and the little socket protocol your desktop app uses to talk to itself.

Two processes and one line of text

Here is the mechanism, in plain terms. When you click a tg:// link, the operating system launches Telegram with the URL as a command-line argument. If Telegram is already running, that second process cannot hand the URL object over — a local socket carries bytes, not objects. So it flattens the URL into text and sends it. That flattening is called serialization. The instance that was already running does the reverse: it parses that text back into instructions.

Telegram's format is simple. Each instruction is a keyword, an argument, and a semicolon that closes it. A link to open becomes:

OPEN:tg://x?a=1;

The receiving side reads the bytes, cuts at every semicolon, and treats each piece as an instruction in its own right. Now look at what the sending side does with a URL: it writes OPEN:, then the URL, then a semicolon. It never escapes anything inside that URL.

So a crafted link, tg://x?a=1;CMD:quit, arrives at the socket as OPEN:tg://x?a=1;CMD:quit; — and the sender genuinely believes that inner semicolon is just punctuation inside a query string. The receiver cuts at every semicolon and sees two instructions: open this URL, then run CMD:quit. One line of text became two commands. That is the injection.

CMD: is harmless by itself — it accepts only two values, show and quit, so the worst it can do is close the app. The damage comes from what OPEN: can reach. It accepts any URL scheme with no filter. And Telegram registers a second scheme internally called interpret:, which is not a system protocol handler at all — it exists only inside Telegram's own code. Hand it the path of a text file and it reads that file, then sends the contents to a chat as an attachment. No confirmation. No check of who asked.

So the whole chain is one clicked link: inject an instruction, point it at interpret:, point that at Telegram's own session files, and the files that are the victim's login get posted to an attacker-controlled chat. Game over.

The general rule, in three questions

Forget Telegram for a second. Every time structured data crosses a boundary — a socket, a queue, a log file, a shell command, an LLM prompt — you have to flatten it. The moment you flatten it, the boundaries that used to be held by the structure become characters in a string. That is not a Telegram quirk. That is what flattening is.

So ask three questions about any format you own:

  1. What is the separator? Semicolon, newline, comma, space, the literal string <END>, a closing tag. Write it down somewhere a new hire can find it.
  2. Can any value contain it? And who controls that value — a user, a filename, a third-party API, a scraped web page?
  3. Who escapes it, and who un-escapes it? If the answer is two different hand-written pieces of code on two sides of a socket, you do not have a format. You have a hope.

If the third answer is vague, you have the Telegram bug. It does not matter that your use case feels internal, or that the value "never" contains a semicolon. Attackers do not read your assumptions.

You have probably shipped this already

  • Logs. A username containing a newline lets someone forge an entire extra log line, complete with a fake timestamp and a fake severity. Your incident timeline is now partly the attacker's.
  • CSV. Commas and quotes inside values, and formula characters at the start of a cell, have caused real damage in spreadsheet pipelines for two decades.
  • HTTP headers. Not escaping a carriage return plus line feed in a header value lets an attacker inject whole new headers, or a second response.
  • Agent tool calls. This is the one to worry about now. When your framework flattens a tool call into XML-style tags or a JSON blob inside a prompt, and the tool's result comes back as free text from the open internet, that result can contain the closing tag. Everything after it reads as a fresh instruction from you. Same bug, different decade.
  • Config files. Two specs called YAML 1.1 and 1.2 disagree about whether the country code NO is a string or the boolean false, and whether 0755 is octal 493 or decimal 755. Same file, two meanings, decided by whichever parser happens to read it.

Three fixes, and what each one costs you

1. Escape the separator

Prefix every literal separator inside a value with an escape character, and have the reader undo it. This is cheap and it is the fix Telegram needed. The cost is symmetry: the encoder and decoder must agree exactly, forever. Classic failure modes are escaping the escape character in the wrong order (so a value containing a backslash breaks the next value), decoding twice, and — the subtle one — version skew. The sending process and the receiving process can be different builds. If one escapes and the other does not, you get the injection back with a side of silent data corruption.

2. Stop having a separator: length-prefix the value

Say the value's length out loud, then read exactly that many bytes no matter what they contain. Netstrings are the canonical example: length, colon, bytes, comma. The receiver never scans for a delimiter, so no delimiter can be faked. The cost is readability — your protocol stops being greppable — and more code on both ends. Note that this fixes the framing only. It does nothing about the second Telegram defect, which is that a perfectly framed instruction could reach a file-reading handler.

3. Use a real parser

Serialize to JSON or protobuf and let a maintained library do the work. This is the right default and it removes a whole category of hand-rolled bugs. The cost is that the boundary moves rather than disappears: you cannot drop raw JSON into a shell command or a CSV cell without escaping again, so you are back at fix 1 at the next hop.

Whichever you pick, implement the encoder and the decoder once, in one shared library, for every language that touches the wire. Telegram's version of the bug exists because a string builder on one side and a loop on the other side were written independently, and neither one owned the question of what a separator means.

The second defect is the one that actually hurts

Formatting bugs are common. Total compromise is not. What turned this into a High severity was the second half of the chain: an internal-only instruction that reads a file and posts it somewhere, reachable from a public chat link with no confirmation and no check of origin.

That handler was written for release tooling — a script that posted a build archive with its changelog as the caption, so nobody had to do it by hand. Perfectly reasonable. Still there years later, still wired up, still reachable.

So "internal" is not an access-control mechanism. It is a naming convention. Ask what the most dangerous instruction on the far side of your boundary can do, then ask who can reach it. If a forged instruction can read a file, delete a row, or move money, fixing the format is not enough — that action needs its own authorization and an explicit confirmation, and an internal-only handler should probably not exist in a release build at all.

How to test this in an afternoon

  1. Round-trip property test. For every separator, and for values that contain it, encode then decode and assert you get exactly one instruction back, with the original value intact.
  2. Count instructions. The sender emits N; the receiver parses M. Any case where N and M disagree is an alarm, not a curiosity. That single assertion would have caught Telegram's bug in CI.
  3. Fuzz the splitter. Feed it the separator inside a value, doubled separators, a trailing separator, 8KB of separators. Worth noting: Telegram truncated each URL at 8,192 bytes after splitting it, which is a useful reminder that a length cap is not escaping.
  4. Enumerate the actions, not just the parser. List everything reachable from a parsed instruction, mark each one read, write, or destructive, and require confirmation on the last two.

What to say in code review

A one-line string concatenation that builds an instruction does not look dangerous. It looks tidy. So ask the question out loud: what characters are legal inside this value, and what happens if a legal character is the separator? If the honest answer is "we would have to see it happen," you have found the bug early — which is the only cheap time to find it.

The good news is that this class of bug is finite. There are maybe five boundaries in your system, each with one separator and one owner. Name them, test the round trip, and count the instructions on both sides. The semicolon stops being the vulnerability the moment somebody owns it.

Key Takeaways

  • Telegram Desktop's CVE-2026-107181 was two bugs stacked: an unescaped semicolon in an internal text protocol, and a file-reading handler that any parsed instruction could reach.
  • Any time structured data is flattened for transport, its internal boundaries become characters in a string. If a value can contain the separator and nobody escapes it, the value can become structure — and structure can become a command.
  • Your logs, CSV exports, HTTP headers, config files, and agent tool-call formats all sit on this boundary. Ask what the separator is and who owns the escaping.
  • Pick one fix and commit to it: escape the separator, length-prefix the value, or use a real parser. Each has a cost — symmetry, readability, or a moved boundary — and none of them help if the action on the far side is dangerous.
  • Add one test that counts instructions sent versus instructions parsed. A mismatch is the whole bug, caught before anyone clicks the link.

[ Call to action ]

Ready to scale your engineering team?

Tell us the roles you need to fill and we'll get back within 24 hours.