Timezone Anomalies: Detecting Impossible Geographic Signals in Signup Fraud
Learn how timezone mismatches, impossible travel patterns, and locale inconsistencies reveal fraudulent signups. Technical guide to geographic anomaly detection.
Why Geography Still Matters for Fraud Detection
When someone signs up for your service, their request carries a surprising amount of geographic information. Their IP address resolves to a location. Their browser reports a timezone. Their system locale suggests a language and region. Their device might even expose GPS coordinates.
The thing is, legitimate users almost always have consistent geographic signals. A user in Tokyo has a Japanese IP, an Asia/Tokyo timezone, a ja-JP locale, and GPS coordinates somewhere in the Kanto region. Everything lines up.
Fraudsters, on the other hand, can rarely get all of these signals to agree. They might route through a VPN in Germany while their browser still reports America/New_York. They might spoof GPS coordinates in London while their system locale is set to ru-RU. These inconsistencies are gold for fraud detection.
The Four Layers of Geographic Identity
Every signup request can be analyzed across four distinct geographic layers. When these layers contradict each other, it is a strong fraud signal.
Layer 1: IP Geolocation
This is the most commonly used signal, and also the most commonly spoofed. IP geolocation databases like MaxMind and IPinfo map IP addresses to approximate physical locations. Accuracy varies: country-level detection is about 99% accurate, city-level drops to around 70-80%.
The key insight is not the location itself, but how it compares to other signals. An IP geolocating to Frankfurt means nothing on its own. But an IP in Frankfurt combined with a browser timezone of America/Chicago? That is worth investigating.
Layer 2: Browser Timezone
The browser exposes the system timezone through the Intl.DateTimeFormat().resolvedOptions().timeZone API. This returns an IANA timezone string like "America/New_York" or "Europe/Berlin". Unlike IP addresses, changing your system timezone requires actually modifying your OS settings, which most automation tools forget to do.
Here is how to collect and transmit this on the client side:
// Collect geographic signals on the client
const geoSignals = {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timezoneOffset: new Date().getTimezoneOffset(),
languages: navigator.languages,
locale: navigator.language,
};
// Send with your signup request
const response = await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
password,
geoSignals,
}),
});Layer 3: Language and Locale
The navigator.languages array reveals the user's preferred languages in priority order. A browser configured with ["en-US", "en"] probably belongs to someone in the United States. A browser with ["ru-RU", "ru", "en-US"] likely belongs to a Russian speaker who added English as a secondary language.
Fraudsters operating at scale rarely customize locale settings per target region. If you are seeing signups that claim to be from Japan but have ["en-US"] as their only language, that is a signal worth scoring.
Layer 4: GPS and Device Location
If your application requests location permissions (or if you are on mobile), you might have access to actual GPS coordinates. While this is the most precise signal, it is also the most easily spoofed on rooted/jailbroken devices. GPS spoofing apps are widely available and can set arbitrary coordinates.
However, GPS spoofing often leaves detectable artifacts. The coordinates might be suspiciously precise (exactly 6 decimal places of a known landmark), the altitude might be missing or zero, and the accuracy radius might be unrealistically tight.
Detecting Timezone vs. IP Mismatches
The most reliable geographic anomaly is a mismatch between the browser timezone and the IP geolocation. Here is a practical implementation:
import { getTimezoneOffset } from 'date-fns-tz';
interface GeoCheck {
ipCountry: string;
ipTimezone: string; // from IP geolocation DB
browserTimezone: string; // from client-side collection
offsetDiffHours: number;
isMismatch: boolean;
score: number; // 0 = no anomaly, 1 = strong anomaly
}
function checkTimezoneConsistency(
ipLat: number,
ipLon: number,
ipTimezone: string,
browserTimezone: string,
): GeoCheck {
const now = new Date();
// Get UTC offsets for both timezones
const ipOffset = getTimezoneOffset(ipTimezone, now);
const browserOffset = getTimezoneOffset(browserTimezone, now);
// Calculate difference in hours
const diffMs = Math.abs(ipOffset - browserOffset);
const diffHours = diffMs / (1000 * 60 * 60);
// Score the anomaly
let score = 0;
if (diffHours === 0) {
score = 0; // Perfect match
} else if (diffHours <= 1) {
score = 0.1; // Adjacent timezone, common for border regions
} else if (diffHours <= 3) {
score = 0.4; // Suspicious but could be travel
} else if (diffHours <= 6) {
score = 0.7; // Likely mismatch
} else {
score = 0.95; // Almost certainly spoofed
}
return {
ipCountry: '', // filled by caller
ipTimezone,
browserTimezone,
offsetDiffHours: diffHours,
isMismatch: diffHours > 1,
score,
};
}In our analysis at BigShield, timezone mismatches of 3+ hours correlate with fraudulent signups about 78% of the time. That number jumps to 91% when combined with datacenter or proxy IP detection.
Impossible Travel Detection
If you track user sessions over time, impossible travel detection becomes incredibly powerful. The concept is simple: if a user logged in from New York 10 minutes ago and is now signing in from Singapore, they did not take a flight. The maximum plausible human travel speed is roughly 900 km/h (commercial aviation). Anything faster is a different device or a VPN switch.
interface TravelCheck {
distanceKm: number;
timeGapMinutes: number;
requiredSpeedKmh: number;
isImpossible: boolean;
}
function checkImpossibleTravel(
prevLat: number, prevLon: number, prevTime: Date,
currLat: number, currLon: number, currTime: Date,
): TravelCheck {
// Haversine distance
const R = 6371; // Earth radius in km
const dLat = (currLat - prevLat) * Math.PI / 180;
const dLon = (currLon - prevLon) * Math.PI / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(prevLat * Math.PI / 180) *
Math.cos(currLat * Math.PI / 180) *
Math.sin(dLon / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distanceKm = R * c;
const timeGapMs = currTime.getTime() - prevTime.getTime();
const timeGapMinutes = timeGapMs / (1000 * 60);
const timeGapHours = timeGapMinutes / 60;
const requiredSpeedKmh = timeGapHours > 0
? distanceKm / timeGapHours
: Infinity;
// 1000 km/h threshold allows for some IP geolocation inaccuracy
const MAX_SPEED_KMH = 1000;
return {
distanceKm: Math.round(distanceKm),
timeGapMinutes: Math.round(timeGapMinutes),
requiredSpeedKmh: Math.round(requiredSpeedKmh),
isImpossible: requiredSpeedKmh > MAX_SPEED_KMH,
};
}One important caveat: impossible travel detection works best for returning users. For brand-new signups, you will not have a previous location to compare against. That is why it is essential to combine this with other signals like timezone checks and device fingerprinting for multi-account detection.
Language and Locale Inconsistencies
Language analysis adds another dimension. We maintain a mapping of countries to expected primary languages and use it to flag inconsistencies:
const COUNTRY_LANGUAGES: Record<string, string[]> = {
US: ['en'],
GB: ['en'],
DE: ['de', 'en'],
JP: ['ja'],
BR: ['pt'],
FR: ['fr', 'en'],
KR: ['ko'],
CN: ['zh'],
RU: ['ru'],
// ... hundreds more
};
function checkLocaleConsistency(
ipCountry: string,
browserLanguages: string[],
): { isConsistent: boolean; score: number } {
const expectedLangs = COUNTRY_LANGUAGES[ipCountry];
if (!expectedLangs) return { isConsistent: true, score: 0 };
const primaryLang = browserLanguages[0]?.split('-')[0];
// Check if the primary browser language matches any expected language
const matches = expectedLangs.includes(primaryLang || '');
if (matches) return { isConsistent: true, score: 0 };
// English is a common secondary language worldwide, lower penalty
if (primaryLang === 'en') return { isConsistent: false, score: 0.3 };
// Complete mismatch (e.g., Russian browser language, Japanese IP)
return { isConsistent: false, score: 0.6 };
}This signal is softer than timezone detection because multilingual users and expats are common. A German user living in Japan might legitimately have a Japanese IP with German browser settings. Weight this signal accordingly, typically between 5-15% of your total geographic score.
GPS Spoofing Detection
When GPS coordinates are available, several heuristics can flag spoofing:
- Altitude check: Spoofed GPS often reports altitude as exactly 0 or omits it entirely. Real GPS readings almost always include altitude data with some variance.
- Precision analysis: Real GPS coordinates have natural noise. If you see coordinates like 51.500000, -0.100000 (suspiciously round), that is likely manual input or a basic spoofing tool.
- Speed consistency: Real GPS readings taken seconds apart show small, natural drift. Spoofed readings are often perfectly static or jump in unnatural patterns.
- Accuracy radius: The GPS accuracy field should be present and reasonable (5-50m outdoors, 20-100m indoors). Missing accuracy data or unrealistically precise values (under 1m) suggest spoofing.
- Cross-reference with IP: If GPS says London but IP says Moscow, one of them is wrong. GPS spoofing is more common than IP spoofing for targeted attacks.
Putting It All Together: A Composite Geographic Score
At BigShield, we combine these signals into a weighted composite score. The weights are tuned based on signal reliability:
interface GeoAnomalyResult {
score: number; // 0-1, higher = more suspicious
signals: string[]; // human-readable explanations
confidence: number; // how confident we are in the result
}
function calculateGeoAnomaly(
tzCheck: GeoCheck,
localeCheck: { isConsistent: boolean; score: number },
travelCheck: TravelCheck | null,
gpsCheck: { isSpoofed: boolean; score: number } | null,
): GeoAnomalyResult {
const signals: string[] = [];
let weightedSum = 0;
let totalWeight = 0;
// Timezone mismatch (weight: 0.4)
weightedSum += tzCheck.score * 0.4;
totalWeight += 0.4;
if (tzCheck.isMismatch) {
signals.push(
`Timezone ${tzCheck.browserTimezone} doesn't match IP location ${tzCheck.ipTimezone}`
);
}
// Locale consistency (weight: 0.15)
weightedSum += localeCheck.score * 0.15;
totalWeight += 0.15;
if (!localeCheck.isConsistent) {
signals.push('Browser language does not match expected region');
}
// Impossible travel (weight: 0.3)
if (travelCheck) {
const travelScore = travelCheck.isImpossible ? 0.95 : 0;
weightedSum += travelScore * 0.3;
totalWeight += 0.3;
if (travelCheck.isImpossible) {
signals.push(
`Impossible travel: ${travelCheck.distanceKm}km in ${travelCheck.timeGapMinutes}min`
);
}
}
// GPS spoofing (weight: 0.15)
if (gpsCheck) {
weightedSum += gpsCheck.score * 0.15;
totalWeight += 0.15;
if (gpsCheck.isSpoofed) {
signals.push('GPS coordinates show signs of spoofing');
}
}
const normalizedScore = totalWeight > 0
? weightedSum / totalWeight
: 0;
return {
score: Math.round(normalizedScore * 100) / 100,
signals,
confidence: totalWeight, // more signals = higher confidence
};
}Real-World Evasion Techniques (and How to Counter Them)
Sophisticated fraudsters know about timezone detection. Here are the evasion techniques we see in the wild and how to counter them:
- Timezone-aware VPNs: Some premium VPN tools automatically set the system timezone to match the exit node. Counter this by checking for other signals like locale, font availability, and keyboard layout.
- Residential proxies: These use real residential IPs, making IP geolocation accurate but misleading. The timezone will still often mismatch unless the fraudster is very careful.
- Browser profile managers: Tools like Multilogin and GoLogin let fraudsters configure timezone, locale, and WebRTC settings per profile. The counter here is behavioral analysis and canvas/WebGL fingerprinting, which are harder to fake.
- VM-based isolation: Running each signup in a fresh VM with matching geographic settings. This is expensive and slow, which limits the scale of attacks. Rate limiting by behavioral patterns still catches these.
Implementation Tips
If you are building geographic anomaly detection into your signup flow, keep these practical tips in mind:
- Collect signals client-side, validate server-side. Never trust client-reported data on its own. Use it as one input among many.
- Handle edge cases gracefully. Some users legitimately use VPNs for privacy. Travelers will show timezone mismatches. Score these as moderate risk rather than outright blocking.
- Update your IP geolocation data regularly. Stale databases cause false positives. BigShield refreshes its IP intelligence weekly.
- Log everything. Geographic signals are incredibly useful for post-incident analysis even when they do not trigger an immediate block.
Geographic anomaly detection is one of the most reliable fraud signals available because it is so hard to get right from the attacker's perspective. Every additional layer of geographic verification multiplies the cost and complexity of mounting a successful attack. If you want to add this to your stack without building it from scratch, BigShield's API evaluates all of these signals (and more) in a single call that returns in under 200ms. Check it out at bigshield.app.