Disclosure: I run BLST VPN Navi, a Japanese guide that answers worries about being watched, hacked, or cut off online, using official sources and daily price data. Some links on the site are affiliate links. This post is about how a small privacy widget works.
My site has a widget that shows what a page can see the moment you open it. It's for people who believe incognito mode hides everything. Showing their own values is faster than explaining.
Three lessons from building it.
1. Don't print the IP into the page
In PHP the visitor's IP is one variable away, so this is tempting:
// Don't do this
echo 'Your IP: ' . esc_html($_SERVER['REMOTE_ADDR']);
If the page is cached — by the server, a caching plugin, or a CDN — the first visitor's IP gets served to everyone after them.
- You show strangers someone else's IP.
- Each visitor thinks it's their own. Someone who just turned on a VPN may conclude it "didn't change" — or the opposite.
For a privacy tool, that's exactly backwards. And it looks fine when you test it yourself, so it's easy to miss.
Fix: leave a placeholder in the HTML and fetch the value from a separate endpoint that is never cached.
add_action('rest_api_init', function () {
register_rest_route('blst/v1', '/whoami', [
'methods' => 'GET',
'callback' => 'blst_whoami',
'permission_callback' => '__return_true',
]);
});
function blst_whoami() {
$ip = blst_whoami_ip();
$r = ['ip' => $ip] + blst_whoami_geo($ip);
nocache_headers(); // per-visitor value
return rest_ensure_response($r);
}
fetch('/wp-json/blst/v1/whoami', { credentials: 'omit' })
.then(function (r) { return r.json(); })
.then(function (d) { document.getElementById('ip').textContent = d.ip || '—'; });
Behind a proxy
Behind a reverse proxy or CDN, REMOTE_ADDR is the proxy. The client address arrives in headers like X-Forwarded-For:
function blst_whoami_ip() {
foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $k) {
if (empty($_SERVER[$k])) continue;
foreach (explode(',', $_SERVER[$k]) as $cand) { // "client, proxy1, proxy2"
$cand = trim($cand);
if (filter_var($cand, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) return $cand;
}
}
return $_SERVER['REMOTE_ADDR'] ?? null;
}
Visitors can set these headers to anything. For a widget that only shows people their own value, spoofing only fools the spoofer. For security decisions — login limits, rate limits — only trust these headers when the request comes through a proxy you control.
2. Don't call third-party IP services from the browser
I first called api.ipify.org from the browser. It's on standard ad-blocker lists, so for anyone running a blocker the request failed and the whole widget stayed empty. Privacy-minded readers — the audience — are the ones running blockers.
Serving the IP from my own domain fixed it.
3. Say honestly what leaves the browser
Country, city, and network owner are looked up server-side through an outside service (ipapi.co) and cached for a day so the same IP isn't queried repeatedly:
$key = 'blst_geo2_' . md5($ip);
// ...on a miss, ask ipapi.co, then:
set_transient($key, $out, $out['country'] ? DAY_IN_SECONDS : 10 * MINUTE_IN_SECONDS);
The note under the widget used to say it stored nothing and sent nothing anywhere. Rereading the code for this post, I realized that was wrong: the IP is sent to an outside service, and the lookup result is kept for a day. The note now says exactly that. A privacy tool that misdescribes its own data handling won't be trusted. I now reread that note whenever I change the code.
What the widget shows: 3 of 18 items can be hidden by a VPN
| Group | Items | Count |
|---|---|---|
| Hidden by a VPN | IP address, approximate location, network owner | 3 |
| Not hidden by a VPN | device/OS, screen size, color depth, GPU, CPU count, memory, touch support, installed fonts, connection-speed hint | 9 |
| Reducible in settings | languages, time zone, cookies, Do Not Track, color-scheme preference, reduced-motion preference | 6 |
A VPN hides 3 of the 18. The rest describe your device, and combined they can follow you across sites (fingerprinting).
I don't show "you are 1 in N" numbers. I have no population data to compare against, so I only state what can be counted.
Takeaways
- Per-visitor values don't belong in cached HTML. Fetch them from an uncached endpoint.
- Forwarding headers can be spoofed. Fine for display; not for security decisions.
- Third-party calls from the browser get blocked — most often for the readers you care about.
- Describe what you send and keep based on the code, and recheck it when the code changes.
Top comments (0)