Your VPN changes your IP address. Your browser is still telling websites where you actually are.
Firefox Add-ons Β |Β Chrome Web Store Β |Β Report Issues Β |Β User Guide
A VPN changes your IP, but your browser still leaks your location through multiple channels: the Geolocation API, timezone offsets, Intl.DateTimeFormat, WebRTC, and much more. Sites cross-reference them against your IP, and when they don't match, you're flagged. Many extensions claim to spoof your location but fall far short.
Blocking geolocation requests is your right, but some sites treat it as evasion and restrict access or flag your account. And if you allow it, your real coordinates go straight to the site. You're stuck choosing between access and privacy.
GeoSpoof gives you full control over what your browser reports. Set your location to match your VPN, mismatch it on purpose for extra obfuscation, or pick somewhere entirely different. GPS coordinates, timezone, Intl locale data, Date APIs, the Temporal API, and WebRTC all stay in sync with whatever you choose.
- VPN Region Sync: Detects your VPN exit IP and sets your spoofed location to match. One click, no manual coordinates.
- Manual Coordinates: Search for a city or enter any latitude/longitude directly. Your location doesn't have to match your VPN.
- Full Signal Alignment: All location signals β geolocation, timezone offset,
Intl.DateTimeFormat, Date getters, Date constructor,Temporal.Now, and WebRTC β report the same place. Sites see one consistent identity instead of mismatched data. - Real-World Timezone Offsets: Offsets are derived from the browser's own IANA timezone database via
Intl.DateTimeFormat, so historical and DST-aware offsets match what a real user in that timezone would produce. No hardcoded offset tables that go stale. - Cross-Browser: Works on Firefox, Chrome, Brave, and Edge. Single codebase, MV3 on both platforms.
- Bypass Hard Gates: Sites that refuse to load without geolocation permission get a clean, consistent response.
- Dev & QA: Test geofenced apps, localized content, or location-aware UIs without leaving your desk.
Note: Use of this tool may violate the Terms of Service of certain websites. This is purely in the interest of legitimate privacy use and development purposes. Use responsibly.
GeoSpoof is designed to work alongside a VPN, not replace one.
- Does NOT spoof your IP address (use a VPN for that)
- Does NOT change browser language or locale
- Does NOT bypass server-side detection (IP, payment info, account history)
- Does NOT track your browsing activity, collect telemetry, or store data on external servers. Some features (city search, VPN sync) call third-party APIs to function. See External Services for exactly what's sent and to whom.
When protection is enabled, GeoSpoof overrides the following browser APIs on every page. All overrides are injected synchronously at document_start before any page JavaScript runs.
| API | Behavior |
|---|---|
navigator.geolocation.getCurrentPosition() |
Returns your spoofed coordinates |
navigator.geolocation.watchPosition() |
Returns your spoofed coordinates |
navigator.geolocation.clearWatch() |
Clears spoofed watch callbacks |
navigator.permissions.query({ name: "geolocation" }) |
Reports permission as "granted" |
| API | Behavior |
|---|---|
Date.prototype.getTimezoneOffset() |
Returns the correct offset for the spoofed timezone, including DST transitions |
Intl.DateTimeFormat() constructor |
Injects the spoofed IANA timezone into all format options |
Intl.DateTimeFormat.prototype.resolvedOptions() |
Returns the spoofed timezone identifier |
Date.prototype.toString() |
Outputs {weekday} {month} {day} {year} {HH:mm:ss} GMT{Β±HHMM} ({timezone long name}) using the spoofed timezone |
Date.prototype.toDateString() |
Outputs {weekday} {month} {day} {year} formatted in the spoofed timezone |
Date.prototype.toTimeString() |
Outputs {HH:mm:ss} GMT{Β±HHMM} ({timezone long name}) using the spoofed timezone |
Date.prototype.toLocaleString() |
Delegates to Intl.DateTimeFormat with the spoofed timezone injected |
Date.prototype.toLocaleDateString() |
Delegates to Intl.DateTimeFormat with the spoofed timezone injected |
Date.prototype.toLocaleTimeString() |
Delegates to Intl.DateTimeFormat with the spoofed timezone injected |
Date.prototype.getHours() |
Returns hours in the spoofed timezone |
Date.prototype.getMinutes() |
Returns minutes in the spoofed timezone |
Date.prototype.getSeconds() |
Returns seconds in the spoofed timezone |
Date.prototype.getDate() |
Returns day of month in the spoofed timezone |
Date.prototype.getDay() |
Returns day of week in the spoofed timezone |
Date.prototype.getMonth() |
Returns month in the spoofed timezone |
Date.prototype.getFullYear() |
Returns year in the spoofed timezone |
new Date(string) (ambiguous strings) |
Adjusts epoch so the date is interpreted in the spoofed timezone instead of the real one |
new Date(year, month, ...) (multi-arg) |
Adjusts epoch so the date is interpreted in the spoofed timezone instead of the real one |
Date.parse(string) (ambiguous strings) |
Same epoch adjustment as the constructor |
Feature-detected at runtime. If the browser supports Temporal, these are overridden:
| API | Behavior |
|---|---|
Temporal.Now.timeZoneId() |
Returns the spoofed IANA timezone identifier |
Temporal.Now.plainDateTimeISO() |
Uses the spoofed timezone when no explicit timezone given |
Temporal.Now.plainDateISO() |
Uses the spoofed timezone when no explicit timezone given |
Temporal.Now.plainTimeISO() |
Uses the spoofed timezone when no explicit timezone given |
Temporal.Now.zonedDateTimeISO() |
Uses the spoofed timezone when no explicit timezone given |
| API | Behavior |
|---|---|
privacy.network.webRTCIPHandlingPolicy |
Configured via the browser's built-in privacy API to prevent IP leaks β no script injection needed |
Overridden functions are disguised to pass standard detection checks used by most fingerprinting scripts:
| Technique | What it does |
|---|---|
Function.prototype.toString |
All overrides return function name() { [native code] } β indistinguishable from real builtins |
| Method shorthand wrapping | Overrides have no prototype property and no [[Construct]], matching native method behavior |
Iframe contentWindow patching |
Iframes get their toString patched synchronously on access, before fingerprinting scripts can grab a clean reference |
| DOM insertion wrapping | appendChild, insertBefore, innerHTML, etc. (11 methods) synchronously patch iframes on insertion |
Privacy caveat: These overrides pass the checks that real-world fingerprinting scripts typically run. Dedicated forensic tools like TorZillaPrint and CreepJS can still detect content-script-level overrides through engine internals, Web Worker context leaks, and timing side-channels. Full undetectability requires browser-level changes (Tor Browser, Mullvad Browser). GeoSpoof does the best that's possible from an extension.
Firefox: https://addons.mozilla.org/en-US/firefox/addon/geo-spoof
Chrome / Brave / Edge: https://chromewebstore.google.com/detail/geospoof/dgdbdodafgaeifgajaajohkjjgobcgje
From GitHub Releases (Firefox self-hosted):
Each release includes a self-hosted signed XPI alongside the AMO submission. The self-hosted XPI uses a 4-segment version (e.g., 1.18.0.42) to avoid collisions with the AMO listing.
- Go to the Releases page
- Download
geospoof-<version>-signed.xpifrom the latest release - In Firefox, open
about:addons - Click the gear icon (β) and select Install Add-on From Fileβ¦
- Select the downloaded
.xpifile
The signed XPI works on standard Firefox with no extra configuration. Once installed, Firefox automatically checks for and installs new versions via the self-hosted update manifest. If you later install from AMO, Firefox will auto-upgrade to it since AMO releases use a higher base version.
Note: An unsigned
geospoof-<version>.xpiis also included in each release for Firefox forks that don't support AMO signatures. Most users should use the signed version.
From source (Firefox):
git clone https://github.com/anthonysgro/geospoof.git
cd geospoof
npm install
cp .env.example .env
npm run build:firefox
npm run start:firefox # Launches Firefox with the extension loadedFrom source (Chrome / Brave / Edge):
git clone https://github.com/anthonysgro/geospoof.git
cd geospoof
npm install
cp .env.example .env
npm run build:chromiumThen load dist/ as an unpacked extension:
- Go to
chrome://extensions(orbrave://extensions,edge://extensions) - Enable "Developer mode"
- Click "Load unpacked" and select the
dist/folder
Or use npm run start:chrome / npm run start:brave to build and launch automatically.
- Click the GeoSpoof icon in your toolbar
- Search for a city, enter coordinates manually, or use "Sync with VPN" to auto-detect your VPN exit region
- Enable "Location Protection" and "WebRTC Protection"
- Refresh open tabs to apply
See USER_GUIDE.md for details.
| Service | When | What's sent | Source |
|---|---|---|---|
| Nominatim (OpenStreetMap) | City search, reverse geocoding | Search query or coordinates | GitHub |
| browser-geo-tz CDN | Timezone resolution | HTTPS range requests for boundary data chunks (coordinates stay local) | GitHub |
| ipify | VPN sync enabled | HTTPS request to detect your public IP | GitHub |
| FreeIPAPI | VPN sync enabled | Your public IP (to geolocate VPN exit region) | Closed source (Privacy Policy) |
VPN Sync privacy note: When you enable "Sync with VPN," your public IP is sent to
api.ipify.organdfreeipapi.comover HTTPS to determine your VPN exit region. Your IP is never saved to disk β it's held only in memory and cleared when you disable VPN sync. See PRIVACY_POLICY.md for full details.
No data is sent to the extension developer. See PRIVACY_POLICY.md.
Requirements: Node.js 18+, npm 9+, Firefox 140+ or any Chromium-based browser
git clone https://github.com/anthonysgro/geospoof.git
cd geospoof
npm install
cp .env.example .envOpen two terminals:
# Terminal 1 β watches your source files and rebuilds on every save
npm run dev
# Terminal 2 β launches Firefox with the extension loaded, auto-reloads on rebuild
npm startThat's it. Edit code, save, Firefox reloads. If something looks wrong, check the browser console (about:debugging β Inspect for background, F12 for content scripts).
For Chromium development, use npm run start:chrome or npm run start:brave instead.
| Command | What it does |
|---|---|
npm run dev |
Watch mode β Vite rebuilds dist/ on every file change |
npm start |
Launch Firefox with the extension loaded from dist/ |
npm run build:dev |
One-time dev build (source maps, console logs) |
npm run build:prod |
One-time production build (minified, no logs) |
npm run build:firefox |
Production build targeting Firefox |
npm run build:chromium |
Production build targeting Chrome/Brave/Edge |
npm test |
Run all tests |
npm run lint:ext |
Lint the extension manifest and files |
npm run validate |
Type-check + lint + format check + tests (run before PRs) |
npm run package |
Firefox production build + zip for AMO submission |
npm run package:chromium |
Chromium production build + zip for Chrome Web Store |
npm run package:xpi |
Production build + package as .xpi for sideloading |
npm run package:source |
Zip source code for AMO review (excludes node_modules, dist, etc.) |
npm run sign:xpi |
Sign the built .xpi via AMO unlisted channel (requires credentials) |
npm run sign:xpi:amo |
Sign the built .xpi via AMO listed channel (requires credentials) |
npm run start:firefox |
Launch Firefox with the extension loaded |
npm run start:chrome |
Build for Chromium + launch Chrome |
npm run start:brave |
Build for Chromium + launch Brave |
npm run start:android |
Launch on Firefox for Android (USB, auto-detects device) |
Requires adb (brew install android-platform-tools) and a USB-connected Android device with Firefox installed.
- Enable Developer Options on your device (Settings β About Phone β tap Build Number 7 times)
- Enable USB Debugging (Settings β Developer Options β USB Debugging)
- In Firefox for Android: Settings β Remote debugging via USB β On
- Connect via USB and run:
npm run build:dev
npm run start:androidThe script auto-detects the first connected device via adb. To target a specific device, pass its ID manually:
npm run start:android -- <device-id>You can find device IDs with adb devices.
A single v* tag push (e.g., v1.18.0) triggers the full release pipeline:
- Build Firefox, package unsigned XPI and source zip
- Inject build version (
1.18.0.{run}) β sign unlisted (self-hosted) - Restore clean dist β strip
update_urlβ sign listed (AMO) with source - Build Chromium
- Create one GitHub Release with all artifacts (signed XPI, unsigned XPI, Chromium zip, source zip)
- Deploy
update.jsonto GitHub Pages for self-hosted auto-updates
The unlisted XPI uses a 4-segment version (e.g., 1.18.0.42) while the AMO submission uses the clean 3-segment version (1.18.0). This avoids AMO's version uniqueness constraint across channels.
Required GitHub Actions secrets:
| Secret | Description |
|---|---|
AMO_JWT_ISSUER |
AMO API key (JWT issuer) |
AMO_JWT_SECRET |
AMO API secret |
To generate credentials:
- Go to the AMO API Keys page
- Sign in with the Mozilla account that owns the extension listing
Releasing:
npm run validate
npm version patch # or minor/major
git push origin main --tagsLocal signing (testing): Set AMO_JWT_ISSUER and AMO_JWT_SECRET in your .env, then:
# Unlisted (self-hosted)
npm run build:firefox
npm run package:xpi
npm run sign:xpi
# Listed (AMO)
npm run build:firefox
npm run package:source
npm run sign:xpi:amoFirefox (AMO):
npm install
cp .env.example .env
npm run packageThis runs a production build and packages the extension into web-ext-artifacts/geospoof-<version>.zip for AMO submission. TypeScript source in src/ is compiled via Vite + esbuild. No hand-minification or obfuscation.
Chromium (Chrome Web Store):
npm install
cp .env.example .env
npm run package:chromiumThis produces web-ext-artifacts/geospoof-chromium-v<version>.zip.
src/
βββ background/ # Settings, geocoding, timezone resolution, VPN sync
βββ build/ # Manifest generator (Firefox/Chromium targets)
βββ content/
β βββ index.ts # Content script (bridge between background and injected)
β βββ injected/ # Page-context API overrides (12 modules)
βββ popup/ # Extension popup UI
βββ shared/ # Shared types and utilities
tests/
βββ unit/ # Unit tests
βββ integration/ # Integration tests
βββ property/ # Property-based tests (fast-check)
Using location spoofing may violate terms of service of streaming, financial, or e-commerce platforms. You are responsible for compliance. See PRIVACY_POLICY.md for full details.
MIT β see LICENSE.
- Nominatim for geocoding
- browser-geo-tz for timezone boundary-data lookup
- BrowserLeaks for testing tools


