External Image URLs as Avatars Are a Wiretap on Every Visitor
Honestly, if a website allows users to fill in an external image URL as an avatar, it's equivalent to installing a wiretap on every visitor
Essentially, an image URL is just initiating a request, so whatever a request can do, this image URL can do. For example:
- Content can be replaced at any time (horror images)
- 401 Basic Auth phishing popup (no longer playable)
- 302 redirect to localhost:8080/admin
- 8-second slow response
- 10MB traffic bomb
- 404 broken image
- Return a piece of JS, like embedding a script in an SVG
Let's do an experiment first: External Link Avatar Security Lab
The page will give you a dedicated UUID avatar link (pointing to my own backend service). You can toggle 7 attack scenario switches and see with your own eyes how the same URL can trick the browser under different scenarios.
The entire demo is based on a simple fact: <img src="..."> is an HTTP request actively initiated by the browser, and whatever the opposing server returns must be accepted as is.
Risk 1: Visitor Information Leakage (Still Effective)
This technique has been used in email marketing for twenty years, commonly known as 'tracking pixels'.
The principle is extremely simple: the attacker places an image on their own server (even just a 1x1 transparent pixel) and fills this URL into the avatar field of some forum.
From then on, every person browsing this post will have their browser send a request to the attacker's server. The attacker's Nginx log clearly records:
203.0.113.45 - [13/Jun/2025 14:23:01] "GET /avatar.png HTTP/1.1" 200
Referer: https://forum.example.com/thread/12345
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0...)
This single log entry exposes:
- IP address (accurate to city level)
- Which post is being viewed (Referer header)
- What device is being used (User-Agent)
- When it was viewed (timestamp)
In my demo, this can be reproduced in the default scenario (with no switches toggled) — the 'Access Log Table' below will display your own IP/UA/Referer in real-time. This log is genuinely received by the backend; the browser cannot bypass it.
If this user posts replies in multiple threads (each loading their avatar), the attacker can even draw a behavioral trajectory map of 'which IP accessed which posts at what time'. GDPR and China's Personal Information Protection Law both classify IP addresses as personal information.
Risk 2: Content Can Be Replaced at Any Time (Still Effective)
The image is on someone else's server. Today they put a normal avatar, tomorrow they quietly swap it for a gory/politically sensitive/advertising image — the platform's user list is instantly compromised.
In the demo, after toggling the 'Replace with Horror Image' switch, refresh the page, and the circular avatar frame immediately turns into a cracked zombie face.
The browser cannot control this at all, because the image URL hasn't changed, and what's returned is indeed an image.
The only defense is: store a copy yourself.
Risk 3: Basic Auth Phishing Popup (Suppressed After 2018)
This is the most interesting one because it demonstrates the process of browser vendors proactively cleaning up after developers.
The attacker's controlled server responds like this:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Please log in to view avatar"
What does the browser do upon receiving this response? Theoretically, it pops up a native username/password dialog. This dialog looks very much like a system dialog, and ordinary users cannot distinguish 'this avatar service is asking for a password' from 'the main site is asking for a password'. Coupled with a realm text like 'Session expired, please log in again', tricking some users into giving up their main site password is not difficult.
However: Since Chrome version 64 (early 2018), followed by Firefox / Safari, cross-origin subresources (img / iframe / script) triggering Basic Auth popups have been uniformly disabled.
So we are safe! But temporarily.
CVE-2017-15400 is the key issue that prompted this fix. The logic behind it: an attacker uses <img src="https://attacker.com/x.png"> to make the user's browser request any third-party service in a logged-in state, popping up a phishing box. After the fix, subresources are blocked, but top-level is still retained — because people do have the legitimate need to 'open a website that requires Basic Auth'.
Risk 4: SSRF — The Disaster Zone of Server-Side Avatar Downloading (Blocked on Frontend, Server Exposed)
The previous three risks are all about 'URLs exposed to the browser'. SSRF is another dimension: if your backend has a feature where 'user submits an avatar URL, and the server downloads this image to make thumbnails/cache', then things get serious.
At this point, it's no longer the browser making the request, but your server. The attacker submits:
http://169.254.169.254/latest/meta-data/iam/security-credentials/
This is the AWS EC2 instance metadata interface — accessing it from within the instance requires no authentication and directly returns temporary credentials (AccessKeyId + SecretAccessKey). Once the attacker gets the credentials, they can operate all your AWS resources.
The 2019 Capital One data breach was this exact pattern: the attacker obtained temporary credentials for a WAF role through an SSRF vulnerability, stealing the credit card application data of 106 million users. Capital One was fined $80 million (Source: US Office of the Comptroller of the Currency OCC Consent Order 2020-080, August 2020).
SSRF Variants:
| Target URL | What Can Be Obtained |
|---|---|
| http://192.168.1.1/admin | Router/Switch Admin Panel |
| http://localhost:6379/ | Redis Unauthorized Access |
| http://internal-jenkins/script | Jenkins Remote Code Execution |
| http://169.254.169.254/... | AWS Temporary Credentials |
| http://metadata.google.internal/... | GCP Service Account Token |
The 'Redirect to localhost:8080' switch in the demo demonstrates an introductory version of SSRF: the user gives you a URL that looks like a .jpg, and when the server fetches it, it finds a 302 redirect to an internal network address. If your avatar fetching code follows redirects by default (which the vast majority of HTTP clients do), it directly hits the internal network.
Risk 5: Resource Exhaustion / DoS (Still Effective)
Two switches in the demo correspond to two types:
- 'Intentional 8-second delay': An image that never returns freezes the entire feed.
Browsers have no mandatory timeout for images — the main thread isn't blocked, but the HTTP/2 connection pool or HTTP/1.1's 6 concurrent connections are maxed out, and subsequent real images can only queue. If the backend does a synchronous fetch (thumbnail scenario), the connection pool is exhausted, and the entire service becomes unavailable.
- '10MB traffic bomb': 100 user visits mean 1GB of traffic, with mobile users bearing the brunt.
An advanced version is the 'decompression bomb': a 1KB GIF that decodes into 10GB of pixel data. If the backend has automatic decoding logic, it directly causes an OOM.
Klee: Kind stranger, help Klee by buying a bomb. Image source: https://www.youtube.com/watch?v=uY0RoMOHKgM
A Common Brainstorm: Can the "Image" Loaded by <img> Actually Be a Piece of JS?
Many people's first reaction is: since the server can return arbitrary bytes, if I return a piece of JavaScript, doesn't the frontend get an XSS entry point? This path is completely dead for four reasons:
- Browsers only accept image byte streams: After getting the response, it goes through the image decoder pipeline (PNG/JPEG/WebP/SVG decoder) and will not enter the JS engine.
- Content-Type cannot be downgraded: Even if you set Content-Type: application/javascript, the browser sees the destination is image and directly determines a type mismatch → refuses to load.
- X-Content-Type-Options: nosniff is now default behavior: Chrome has enforced this for all
<img>since 2019. - No DOM context: Even if the script could execute (impossible), it cannot access cookies / document, because the image is an isolated fetch, not attached to any document tree.
This is one of the strongest moats built by the browser's same-origin policy + MIME type separation over the past 20 years.
But if you switch to SVG, the script is completely different —
Risk 7: SVG is a Script Disguised as an Image (XSS with a Different Tag)
SVG has a .svg suffix, MIME is image/svg+xml, looks like an "image" just like PNG/JPEG, but is essentially an XML document that can contain <script>:
In the demo, after toggling the 'Embed Script in SVG' scenario, a comparison demo area appears on the page. The left side loads with <img> (static rendering, script does not execute), and the right side loads with a button click using <object> — the same URL, the same SVG, immediately triggers an alert + injects a red banner at the top of your page, letting you see firsthand how XSS happens.
Because essentially, for the same file, the result is completely different depending on what tag is used to load it:
| Tag | Browser Parsing Mode | Can Execute JS? |
|---|---|---|
| image decoder (sandbox) | ❌ Script Disabled | |
| SVG Full Mode | ✅ | |
| full document | ✅ | |
| Same as object | ✅ | |
| Server-side inline SVG into main page | inline | ✅ With host privileges |
Therefore:
- User submits avatar → Backend fetches it, finds it's SVG → Saves it directly without decoding/redrawing
- Frontend loads with
<img>→ Looks safe - But as long as some corner of the platform uses a non-
<img>tag (export PDF / email preview / admin review page / rich text preview / Markdown rendering) → Immediate XSS
Of course, I also tried for you whether it's possible to read the visitor's clipboard. It's not possible.
The clipboard path is blocked by modern browsers (navigator.clipboard.readText() requires active user authorization + page focused + transient activation), but there's still plenty that can be done after gaining JS execution rights:
- Read document.cookie (provided it's not HttpOnly)
- Inject fake login forms
- Listen to keyboard input with keydown
- Steal localStorage / sessionStorage
- Perform CSRF with the user's Cookie
GitHub's emergency fix in 2017 was this exact pattern: user embeds SVG in README → SVG embeds <foreignObject> to write full HTML → Full XSS (GitHub Security Advisories, 2017).
The Correct Approach: Treat User URLs as Raw Material, Pass Through a Complete Sanitization Process
From weakest to strongest protection:
(Prevent Referrer leakage)
(Don't send Cookies)
- Block .svg suffix
- CSP img-src whitelist
These can block some, but the IP leakage problem is forever — as long as <img> goes out, it always carries the IP, determined by TCP/IP.
The correct solution: Server-side download + validation + redraw + rehost.
Complete process (visual flowchart at the bottom of the demo):
- Protocol Whitelist: Only allow https://, reject http / file / data / javascript / gopher
- SSRF Protection: After DNS resolution, verify IP is not in reserved blocks like 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16 (cloud metadata)
- Disable Redirects: Turn off follow redirects when fetching server-side, or re-validate IP for every hop
- Content-Type + Magic Number Double Check: It's not enough for the response header to say image/jpeg; also verify the file header FF D8 FF
- Size / Dimension Limit: Directly reject if over 5MB or 4096×4096
- Decode and Redraw: Decode with ImageIO / sharp and re-encode, stripping EXIF, SVG scripts, steganographic payloads
- Content Moderation: Connect to porn/terror/copyright detection
- Rehost to Own CDN: Serve externally under your own domain
How Major Platforms Do It
| Platform | Method | Allow External URL? |
|---|---|---|
| GitHub | After upload, store in own CDN (avatars.githubusercontent.com) | No |
| Server processes then stores in own object storage | No | |
| Discord | After upload, store in CDN (cdn.discordapp.com) | No |
| Gravatar | Email hash mapping, fixed URL format | No (It is itself an avatar CDN) |
| Some old forums (Discuz/phpBB) | Allow filling in external URL | Yes (All above risks exist) |
Almost all mature platforms have chosen 'rehost to own CDN'. The maintenance cost buys the complete elimination of this attack surface.
Finally
Allowing users to fill in an external image URL as an avatar = allowing users to embed a request to any third-party server on your page. For viewers, it's a privacy leak (IP, fingerprint, behavior tracking); for the server, it's SSRF (internal network probing, cloud credential theft).
Interactive demo → External Link Avatar Security Lab
Data Sources:
- Capital One SSRF Data Breach Fine: US Office of the Comptroller of the Currency, Consent Order 2020-080, August 2020
- Chrome Subresource Basic Auth Blocking: CVE-2017-15400 / Chromium bug 435547, released with Chrome 64 in 2018
- GitHub SVG Security Fix: GitHub Security Advisories, 2017
- OWASP SSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- MDN Referrer-Policy: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy