CVE-2026-107181

Telegram Desktop — IPC record injection → arbitrary file exfiltration

The bug in one paragraph

Telegram Desktop runs as a single instance. A second launch forwards its URL to the first over a local socket, and both sides treat ; as the command separator. The sender never escapes it:

commands += u"OPEN:"_q + url.toString(QUrl::FullyEncoded) + ';';

QUrl::FullyEncoded leaves ; alone — RFC 3986 calls it a legal sub-delimiter. So a ; inside the URL opens a second command. A second flaw compounds it: the OPEN: handler applies no scheme filter, so an injected record reaches the internal interpret: scheme, which reads a file named in an instruction file and posts it to a channel — with no authorization check and no confirmation dialog.

AffectedTelegram Desktop through 7.2.8
Fixed7.2.9 (2026-09-16)
Still vulnerablematerialgram v7.0.5.1 — the fork has not taken the fix

1. Record decomposer

Paste a tg:// URL and see how the receiver splits it. This is arithmetic on a string — nothing is launched.

Why %3B does not work

Only a raw ; splits. QUrl::FullyEncoded does not decode an already-encoded sequence, so %3B stays %3B and the receiver never sees a separator. Try both forms above.

2. Instruction file

The path after interpret: points at the instruction file, not at the file being stolen. The target is named inside it.

channel: 4316121815
file: C:/Users/you/Documents/target.txt
caption: lab
KeyMeaning
channel:destination — bare ID, no -100 prefix. Must be a channel or supergroup the victim is already in.
file:what gets stolen — any path the process can open
caption:message text
topic:forum topic id, optional
from:owner check — omit it and no check runs at all

The CRLF trap

The parser splits on \n only. A file saved by Notepad uses CRLF, so every line keeps a trailing \r — and file: silently points at a path that does not exist. channel: still works, because toULongLong() stops at the \r. The result looks exactly like a mistyped path. Save as LF.

3. Launcher

This hands a tg:// URL to your OS, the same way a browser does for any link. It only reaches the IPC socket if a Telegram instance is already running — a link clicked inside Telegram is handled in-process and never touches the socket, which is exactly why the delivery path has to be a browser.

Enter a path, then preview or launch.

4. Reading the error dialogs

Each failure stops at a different point, so the message tells you exactly how far the payload travelled.

DialogReached
Could not open interpret fileChain works. The instruction file itself is missing.
Could not find file with pathInstruction file parsed. The file: target is missing — or CRLF.
Could not find channel with idFile read. The channel is not loaded in this session.
Invalid command: <line>Unknown key. Only the five above are accepted.
Wrong current userfrom: was present and did not match.
no dialog, file in channelFull chain.

5. Source map

core/sandbox.cpp:529            OPEN: -> startUrls.append(...)       no scheme filter
core/application.cpp:1233       scheme() == "interpret"              extracted
mainwidget.cpp:3253             InterpretSendPath(...)               unconditional
support/support_helper.cpp:673  QFile f(path) ... api().sendFiles()  no dialog

The 7.2.9 fix has four independent layers. The decisive one is the third — the interpret: branch was removed from checkStartUrls() entirely. Escaping alone would still leave a local attacker able to write to the socket directly.

6. Materials

Local reproduction needs a Windows host and a pinned 7.2.8 build. Note that the portable build updates itself: -noupdate stops it applying a staged update but not downloading one, so assert the version on every run.