TL;DR: I'm just wondering if there are any other ways for the server to obtain the client's timezone aside from using Date()
and Intl.DateTimeFormat
.
The detection test I want to pass is at .html. I used spoof timezone extension, but the test always reveals my real timezone. I then dug into the the extension's code and added some anti-bypass mechanisms, yet the problem persists. I tried everything I can do to pass the timezone test but the website always detects my real timezone. I have no idea how it managed to do that.
Here is the approach for timezone spoofing.
Date Spoofing I created a class
SpoofDate
that extendsDate
and override thegetTimezoneOffset
function.Handling Parsing Logic When called with
new
, if the argument passed is a string, adjust the parsing logic based on different formats, since MM/DD/YYYY will be parsed as local time byDate.parse()
and YYYY-MM-DD will be treated as UTC time.Intl Spoofing I created a class
SpoofDateTimeFormat
extendsIntl.DateTimeFormat
and override funcitonresolvedOptions
Then I set Date = new Proxy(SpoofDateWrapper, {...});
to replace the global Date. Same for Intl.DateTimeFormat
.
Which confuses me most is that despite everything seeming correct, the website can always report my real timezone with "reported_location mismatch: [spoofed timezone], [my real timezone]". I checked the JavaScript source code of the detection part of the website (pd-lib.js) and found no clues. It appears that the server directly returned the result.
To reproduce, follow these steps:
Change your system timezone to one that does not match your IP (e.g., UTC+12:00 Pacific/Fiji). Or you can use a proxy/VPN. The key is to create a system timezone and IP timezone mismatch enviroment, to show that the test retrieves your system timezone rather than the timezone based on your IP.
(Optional) Modifiy the JavaScript code at the buttom: set your real timezone and offset at
Object.defineProperties
.Managed to autorun the JavaScript code before
document_start
(for example, using a userscript manager like Tampermonkey).Visit the proxy detection website. After the test completes, check the Timezone Test section, and you will see Proxy Detected.
You can also click More Info and it will show you the data it collected from Date()
, Intl.DateTimeFormat
and your IP. However, the key "reported_location"
of the Json will always display your system timezone (Pacific/Fiji, if you followed the first method in step 1), regardless of the timezone and offset set in step 2.
{
const port = document.createElement('span');
port.id = 'stz-obhgtd';
document.documentElement.append(port);
const OriginalDate = Date;
// prefs
const prefs = {
updates: [] // update this.#ad of each Date object
};
Object.defineProperties(prefs, {
'offset': {
get() {
// return parseInt(port.dataset.offset);
return parseInt("0");
}
},
'timezone': {
get() {
// return port.dataset.timezone;
return "Etc/GMT";
}
}
});
port.addEventListener('change', () => prefs.updates.forEach(c => c()));
/* Date Spoofing */
class SpoofDate extends Date {
#ad; // adjusted date
#isNow = false; // whether it's getting the current time, i.e., new Date()
#sync() {
const offset = (prefs.offset + super.getTimezoneOffset());
this.#ad = new OriginalDate(this.getTime() + offset * 60 * 1000);
}
constructor(...args) {
super(...args);
if (args.length === 0) {
this.#isNow = true;
}
// Ensure the instance's prototype is SpoofDate.prototype
Object.setPrototypeOf(this, SpoofDate.prototype);
prefs.updates.push(() => this.#sync());
this.#sync();
}
getTimezoneOffset() {
return 0 - prefs.offset;
}
/* to string (only supports en locale) */
toTimeString() {
if (isNaN(this)) {
return super.toTimeString();
}
const parts = super.toLocaleString.call(this, 'en', {
timeZone: prefs.timezone,
timeZoneName: 'longOffset'
}).split('GMT');
if (parts.length !== 2) {
return super.toTimeString();
}
const a = 'GMT' + parts[1].replace(':', '');
const b = super.toLocaleString.call(this, 'en', {
timeZone: prefs.timezone,
timeZoneName: 'long'
}).split(/(AM |PM )/i).pop();
return super.toTimeString.apply(this.#ad).split(' GMT')[0] + ' ' + a + ' (' + b + ')';
}
/* only supports en locale */
toDateString() {
return super.toDateString.apply(this.#ad);
}
/* only supports en locale */
toString() {
if (isNaN(this)) {
return super.toString();
}
return this.toDateString() + ' ' + this.toTimeString();
}
toLocaleDateString(...args) {
args[1] = args[1] || {};
args[1].timeZone = args[1].timeZone || prefs.timezone;
return super.toLocaleDateString(...args);
}
toLocaleTimeString(...args) {
args[1] = args[1] || {};
args[1].timeZone = args[1].timeZone || prefs.timezone;
return super.toLocaleTimeString(...args);
}
toLocaleString(...args) {
args[1] = args[1] || {};
args[1].timeZone = args[1].timeZone || prefs.timezone;
return super.toLocaleString(...args);
}
/* get */
#get(name, ...args) {
if (this.#isNow) {
// Directly return the current time
return super[name].call(this.#ad, ...args);
}
else
return super[name].call(this, ...args);
}
getDate(...args) {
return this.#get('getDate', ...args);
}
getDay(...args) {
return this.#get('getDay', ...args);
}
getHours(...args) {
return this.#get('getHours', ...args);
}
getMinutes(...args) {
return this.#get('getMinutes', ...args);
}
getMonth(...args) {
return this.#get('getMonth', ...args);
}
getYear(...args) {
return this.#get('getYear', ...args);
}
getFullYear(...args) {
return this.#get('getFullYear', ...args);
}
/* set */
#set(type, name, args) {
if (type === 'ad') {
const n = this.#ad.getTime();
const r = this.#get(name, ...args);
return super.setTime(this.getTime() + r - n);
}
else {
const r = super[name](...args);
this.#sync();
return r;
}
}
setHours(...args) {
return this.#set('ad', 'setHours', args);
}
setMinutes(...args) {
return this.#set('ad', 'setMinutes', args);
}
setMonth(...args) {
return this.#set('ad', 'setMonth', args);
}
setDate(...args) {
return this.#set('ad', 'setDate', args);
}
setYear(...args) {
return this.#set('ad', 'setYear', args);
}
setFullYear(...args) {
return this.#set('ad', 'setFullYear', args);
}
setTime(...args) {
return this.#set('md', 'setTime', args);
}
setUTCDate(...args) {
return this.#set('md', 'setUTCDate', args);
}
setUTCFullYear(...args) {
return this.#set('md', 'setUTCFullYear', args);
}
setUTCHours(...args) {
return this.#set('md', 'setUTCHours', args);
}
setUTCMinutes(...args) {
return this.#set('md', 'setUTCMinutes', args);
}
setUTCMonth(...args) {
return this.#set('md', 'setUTCMonth', args);
}
}
/* Bypass detection */
// Use a wrapper function to simulate native Date's behavior
function SpoofDateWrapper(...args) {
// If not called with new, then return new SpoofDate(...args).toString()
if (!(this instanceof SpoofDateWrapper)) {
return new SpoofDate(...args).toString();
}
return new SpoofDate(...args);
}
// Make SpoofDateWrapper inherit from SpoofDate's prototype chain
SpoofDateWrapper.prototype = SpoofDate.prototype;
SpoofDateWrapper.prototype.constructor = SpoofDateWrapper;
// Sync SpoofDateWrapper's static properties and methods from native Date (OriginalDate)
Object.getOwnPropertyNames(OriginalDate).forEach(prop => {
if (!(prop in SpoofDateWrapper)) {
try {
const desc = Object.getOwnPropertyDescriptor(OriginalDate, prop);
Object.defineProperty(SpoofDateWrapper, prop, desc);
} catch (e) {
// Some properties may not be defined, ignore errors
}
}
});
// Set the constructor's length to 7
Object.defineProperty(SpoofDateWrapper, "length", { value: 7 });
// Set the name property to "Date"
Object.defineProperty(SpoofDateWrapper, "name", { value: "Date" });
// Override toString so that it returns the native Date's code string
SpoofDateWrapper.toString = function () {
return "function Date() { [native code] }";
};
// Set SpoofDate's prototype to the native Date's prototype (so instances have native Date's methods)
SpoofDate.prototype = OriginalDate.prototype;
// Override getTimezoneOffset method's toString to mimic native code output
Object.defineProperty(SpoofDate.prototype.getTimezoneOffset, 'toString', {
value: function () {
return 'function getTimezoneOffset() { [native code] }';
},
writable: false,
configurable: false
});
/* override */
// Use Proxy to replace the global Date, intercepting both function calls and constructor calls
self.Date = new Proxy(SpoofDateWrapper, {
apply(target, thisArg, argumentsList) {
// When called as a function
return target(...argumentsList);
},
construct(target, argumentsList, newTarget) {
// When called with new
if (argumentsList.length === 1 && typeof argumentsList[0] === "string") {
const dateStr = argumentsList[0];
// Adjust parsing logic based on different formats:
if (/\d{2}\/\d{2}\/\d{4}/.test(dateStr)) {
// MM/DD/YYYY format: use custom local time parsing logic
const timestamp = OriginalDate.parse(dateStr);
return new target(timestamp);
}
else if (/\d{4}-\d{2}-\d{2}/.test(dateStr)) {
// YYYY-MM-DD format: treat as UTC time
const timestamp = OriginalDate.parse(dateStr);
now = new OriginalDate();
const offset = (prefs.offset + now.getTimezoneOffset());
return new target(timestamp + offset * 60 * 1000);
}
}
return new target(...argumentsList);
}
});
/* Intl Spoofing */
class SpoofDateTimeFormat extends Intl.DateTimeFormat {
constructor(...args) {
// Ensure the options object exists
if (!args[1]) {
args[1] = {};
}
// If no timeZone is specified, use the value from port
if (!args[1].timeZone) {
// args[1].timeZone = port.dataset.timezone;
args[1].timeZone = prefs.timezone;
}
super(...args);
}
// Override resolvedOptions method to return spoofed timezone information
resolvedOptions() {
let options = super.resolvedOptions();
// options.timeZone = port.dataset.timezone;
options.timeZone = prefs.timezone;
return options;
}
}
// Use Proxy to wrap SpoofDateTimeFormat, ensuring the call behavior is identical to native
Intl.DateTimeFormat = new Proxy(SpoofDateTimeFormat, {
apply(target, thisArg, args) {
return new SpoofDateTimeFormat(...args);
},
construct(target, args) {
return new SpoofDateTimeFormat(...args);
}
});
}
/* for iframe[sandbox] */
window.addEventListener('message', e => {
if (e.data === 'spoof-sandbox-frame') {
e.stopPropagation();
e.preventDefault();
try {
e.source.Date = Date;
e.source.Intl.DateTimeFormat = Intl.DateTimeFormat;
}
catch (e) { }
}
});
Full code: