Technical write-up
Google Messages — findings & analysis
Summary & severity
Google Messages (com.google.android.apps.messaging) was examined on a Samsung Galaxy A26 (SM-A266B), Android 16, build BP4A.251205.006, running the package messages.android_20260910_03_RC02.phone_samsung_dynamic (version code 323089900). The transport under test was RCS. Analysis is static, against the exact installed APK (base APK SHA-256 9e869a68…ff60b2, signature verified as a genuine Google build), combined with behaviour observed on that one device.
| ID | Finding | CWE | CVSS 3.1 | Severity |
| F-PREVIEW | IP/metadata leak via on-device link preview | 200/359 | 4.3 | Medium |
| F-UA | Preview request carries a spoofed, remotely configured user-agent (folded into F-PREVIEW) | 451 | — | Context |
F-PREVIEW · CWE-200 / CWE-359
IP/metadata leak via on-device link preview
Medium · CVSS 4.3
Description
When a message containing a URL is handled, Google Messages generates the link preview by making an HTTP(S) GET to that URL from the device, not through a server-side proxy. The server that owns the link therefore receives a request originating from the recipient's current network egress: its IP address, the preview user-agent, Accept-Language and the request time. Follow-up requests for an og:image or a favicon can additionally carry a Dalvik user-agent that exposes the Android version, device model and build number.
How the fetch is actually triggered
This is the point that most needs stating precisely, because it sets the severity. The path is two-stage:
- On receipt,
request_link_preview work is scheduled after the message database commit (dfvl/bcnl → btlr.b()). For a new link, the first pass can insert a deferred preview row and return before any network request (btln).
- When the conversation becomes interactive — i.e. the chat is opened —
onMessageListInteractive starts an observer (ahxw → aied) that watches the deferred rows for that conversation and schedules a second, deferred=true job (aieb → btlr.a()), which can proceed to the fetch after the gates below.
The observer runs in a background coroutine scope with no visible unregister when the chat is left. That is the code-level reason a fetch can be observed with the screen locked and the app backgrounded after the chat has been opened once. A fetch with the conversation never opened is not established by the visible code and is not claimed here.
Gates that must pass first
- Preview setting. The automatic-preview setting must be on (
aqck). Google Messages exposes “Show all previews” / “Show only web link previews” / off.
- Sender trust. The sender must satisfy a trust predicate (
btez.m()): a populated contact lookup_key is one accepted condition, with two other participant states. A separate flag path can instead count manual preview loads (APK-default threshold 3, stored per participant). The APK default enable_link_preview_untrusted_senders is false. Opening a chat or marking a message read is not the same as a manual preview load; live remote-config values were not read.
- URL scheme. The parser accepts only
http/https (it rewrites ws:/wss:); it rejects file:// and content://, and the image path runs through that same HTTP-only client.
Impact, bounded to evidence
The link's server learns the recipient's current egress IP, user-agent, language and timing, and — via image/favicon follow-ups — device model, OS version and build. Because the preview is keyed by message ID, not URL, the same link in a new message produces a new fetch, so a trusted sender can take repeated readings over time and build a coarse location/network trail. In the archived March run the device used a VPN, so the address observed was a VPN egress, not a residential IP. No access to other apps, no local-service reachability, no account takeover and no code execution were demonstrated or are claimed.
Who counts as “trusted”?
“Saved contact” is broader than it sounds. It is not only the people you chat with — it is any number your phone already knows: a shop, a delivery service or a bank whose number you once saved. Whether verified business senders — the bulk SMS/RCS services companies use to send you links, which arrive with a business name and no “unknown sender” warning — clear the same bar was not tested, so it stays here as a question for Google. Either way, the set of senders who could trigger an automatic fetch is wider than “your friends”.
CVSS 3.1 breakdown
AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N → Base 4.3 (Medium)
The impact is disclosure of an IP address plus a user-agent and device metadata, which is C:Low — this caps the score at Medium no matter how little interaction each individual fetch needs. UI:R reflects the code: the fetch chain is driven by the conversation having been opened. The earlier “Critical / zero-click” framing is not supportable: it rests on an UI:N, never-opened path that the visible code does not establish, and on a C:High impact that an IP-and-metadata leak does not reach.
What a fix looks like
- Fetch link previews through a Google-side proxy, so the link server sees Google's infrastructure rather than the recipient's device — the approach already used for other preview surfaces.
- If on-device fetching is kept, make it strictly interactive (an explicit per-message tap), and do not let a background observer re-drive fetches for a conversation after it is closed.
- Document the data flow plainly in the privacy help, so “automatic previews” is not read as “previews that cannot reveal your IP”.
Severity
A note on the score
A CVSS base score is driven by the attack vector, the interaction needed and the confidentiality/integrity/availability impact — not by how alarming a finding sounds. A privacy leak that reveals an IP address and some device metadata is C:Low, so it cannot reach High no matter how little the victim has to do. “Zero-click” would only lower the interaction metric, not raise the impact ceiling — and here the code does not even support the zero-click, never-opened variant, so the honest vector is UI:R, Base 4.3.
This finding is real and worth fixing — and honestly Medium. An earlier draft labelled it Critical; that label is inconsistent with its own CVSS and with what a single IP/metadata disclosure actually is. I would rather publish a defensible score than an inflated one.
What this is — and is not
To keep the record clean, these were checked and are not claimed as part of this finding:
- No cross-app or sandbox access. An HTTP
GET from the preview client is not a read of another app's data; the Android app sandbox is not crossed, and no such crossing was demonstrated.
- No account takeover, no code execution. The RCE surface was reviewed (dynamic code loading,
exec/ProcessBuilder sinks, deserialization, WebView bridges, the HTML parser) and nothing reachable from an incoming message was found.
- No universal claim. One device, one Android version, one RCS setup was tested. “Every Google Messages user” is not evidenced here.
- Not a version string. The observed
GoogleMessages/20.2 user-agent comes from remote configuration and is not a build identifier.
- A handful of manifest leads (custom permissions declared at
normal protection; a debug receiver exported without a permission) are noted for completeness but are untested leads, not findings, and are not scored.
Check it yourself
No screenshots are published here; the finding stands on the code path above, and anyone can reproduce it with their own endpoint. The idea in one paragraph:
- Point to a URL you control that logs each incoming request (IP, user-agent, time). A ready-made request logger such as webhook.site (or RequestBin, or a one-line server of your own) works — it gives you a unique URL and shows every hit live. Use a fresh, unique path per message so each fetch maps to exactly one message.
- From a second account saved as a contact, send that link into a conversation you have opened before. Watch your log: a request arrives from the recipient phone's egress, carrying a
GoogleMessages user-agent.
- For a rigorous result, record the phone's lock/Doze/process state on the same clock as the server log, and run once without a VPN so the egress you see is the real one.
To prevent these requests, disable automatic web link previews in settings. “Show only web link previews” does not stop these web fetches. Be aware that a saved contact can probe your network with a link.
Disclosure timeline
- 2026-03-14
Reported to the Google Mobile VRP — on-device link-preview fetch leaking the recipient's IP and device metadata (CWE-200/359), with HTTP logs and screenshots from my own device and account (issu.ee/492756092).
- 2026-03-17
Google triages and closes the report. No remediation commitment followed.
- 2026-10-29
Re-examined the current installed release by static analysis of the exact APK. The on-device preview fetch and the two-stage trigger are unchanged; the behaviour is still present.
What would count as fixed
I would rather update this page than defend it. The page will say “fixed” when link previews are fetched through Google's own infrastructure (so the link server no longer sees the recipient's IP), or when on-device fetching is made strictly interactive with no background re-driving after a conversation is closed — in the shipping release, not only an internal build.
All testing was performed on my own device and my own accounts, against endpoints I control. No other person's device, account or data was involved. This write-up contains no malware and no weaponised exploit; the reproduction is an ordinary link sent to an endpoint you own. Identifiers are truncated.