Privacy Shield - 2.7

Aggressive privacy protection: blocks trackers, strips tracking params, spoofs fingerprinting vectors — without breaking normal site functionality.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Privacy Shield - 2.7
// @namespace    https://github.com/j0tsarup
// @version      2.7
// @description  Aggressive privacy protection: blocks trackers, strips tracking params, spoofs fingerprinting vectors — without breaking normal site functionality.
// @author       </j0tsarup>
// @match        *://*/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

/**
 * ============================================================
 *  PRIVACY SHIELD - v2.7
 *  Author : </j0tsarup>
 * ============================================================
 *
 *  WHAT THIS SCRIPT DOES
 *  ---------------------
 *  Layer 1   Tracking Parameter Removal
 *            Strips utm_*, fbclid, gclid, and 30+ other
 *            tracking tokens from the current page URL.
 *
 *  Layer 2   Fingerprint Spoofing (Canvas / WebGL / Audio)
 *            Canvas pixel noise, WebGL vendor/renderer mask,
 *            AudioContext sample noise, Navigator hardware
 *            property overrides (configurable, safe).
 *
 *  Layer 3   Network Request Blocking
 *            Hostname-anchored XHR + fetch interception for
 *            25+ known tracker domains. No DOM removal.
 *
 *  Layer 4   Search Engine Link Cleaning
 *            Unwraps Google, Bing, Yahoo, DuckDuckGo redirect
 *            URLs so clicks go direct to the destination.
 *
 *  Layer 5   ClientRects Spoofing
 *            Smart probe detection — noises only tiny or
 *            off-screen elements, not normal layout calls.
 *
 *  Layer 6   Font Fingerprinting Protection
 *            Patches measureText() with stable session noise.
 *
 *  Layer 7   WebGPU Fingerprinting Protection
 *            Masks adapter vendor, architecture, device info.
 *
 *  Layer 8   WebRTC Host Candidate Filtering
 *            Drops host ICE candidates to prevent IP leaks.
 *
 *  Layer 9   Client Hints Blocking
 *            Spoofs navigator.userAgentData with generic
 *            brand/platform values.
 *
 *  Layer 10  Timezone & Intl Spoofing  (FIXED v2.7)
 *            Spoofs ONLY the timeZone field. Caller locale is
 *            always preserved — no longer forces en-US, which
 *            was breaking non-English sites and currency/number
 *            formatting globally.
 *
 *  Layer 11  Network Information API Spoofing
 *            Spoofs navigator.connection with neutral values.
 *
 *  Layer 12  SpeechSynthesis Spoofing  (FIXED v2.7)
 *            Returns empty voice list. No longer blocks the
 *            voiceschanged event, which was causing sites using
 *            legitimate async TTS to hang indefinitely.
 *
 *  SHORTCUTS
 *  ---------
 *  None - runs silently in the background.
 *  Type  __PS_STATS__  in the browser console for live counts.
 *
 *  LIMITATIONS
 *  -----------
 *  - Timezone fix may affect calendar/scheduling apps.
 *  - First-party proxied trackers cannot be blocked safely.
 *  - User-Agent spoofing is intentionally omitted.
 *
 *  CHANGELOG
 *  ---------
 *  v2.7  Minor release — Layer 10 + 12 breakage fixes
 *        * Layer 10: Intl.DateTimeFormat wrapper no longer
 *          forces en-US locale. Caller locale is passed through
 *          unchanged. Only timeZone is spoofed. Fixes broken
 *          date/currency/number formatting on non-English sites.
 *        * Layer 10: toLocaleString family no longer overrides
 *          locale. Only injects timeZone into options.
 *        * Layer 12: Removed voiceschanged event blocking.
 *          Sites using async TTS were hanging waiting for an
 *          event that would never fire.
 *  v2.6  Added layers 10 (Timezone), 11 (Network API),
 *        12 (SpeechSynthesis)
 *  v2.5  Layer 3 false-positive fix (Chewy white screen)
 *  v2.4  Layer 2 Navigator stability fix
 *  v2.3  Diagnostic build with per-layer toggles
 *  v2.2  Removed iceTransportPolicy + SCRIPT DOM removal
 *  v2.1  ClientRects smart probe detection
 *  v2.0  Added layers 5-9
 *  v1.0  Initial release
 *
 * ============================================================
 */

(function () {
    'use strict';

    /* ----------------------------------------------------------
     *  STATS  (console: __PS_STATS__)
     * ---------------------------------------------------------- */
    const stats = {
        blockedRequests: 0,
        strippedParams:  0,
        cleanedLinks:    0,
        spoofedRects:    0,
        blockedRTCLeaks: 0,
    };
    window.__PS_STATS__ = stats;

    function safeDefine(obj, prop, value) {
        try {
            const desc = Object.getOwnPropertyDescriptor(obj, prop);
            if (!desc || desc.configurable) {
                Object.defineProperty(obj, prop, {
                    get: () => value,
                    configurable: true,
                    enumerable:   true,
                });
            }
        } catch (_) {}
    }

    function noise(seed, magnitude) {
        const x = Math.sin(seed + 1) * 10000;
        return (x - Math.floor(x) - 0.5) * magnitude;
    }

    /* ----------------------------------------------------------
     *  LAYER 1  TRACKING PARAMETER REMOVAL
     * ---------------------------------------------------------- */
    const TRACKING_PARAMS = new Set([
        'utm_source','utm_medium','utm_campaign','utm_term','utm_content',
        'utm_id','utm_source_platform','utm_creative_format','utm_marketing_tactic',
        'gclid','gclsrc','dclid','gbraid','wbraid',
        'fbclid','fb_action_ids','fb_action_types','fb_source',
        'mc_cid','mc_eid','msclkid',
        '_hsenc','_hsmi','__hssc','__hstc','__hsfp','hsCtaTracking',
        'mkt_tok','s_cid','icid',
        'ref','referrer','source','affiliate','zanpid',
        'origin','igshid','twclid','li_fat_id','yclid',
        'email_source','CMP',
    ]);

    function cleanURL(urlStr) {
        let url;
        try { url = new URL(urlStr); } catch { return urlStr; }
        let stripped = 0;
        TRACKING_PARAMS.forEach(p => {
            if (url.searchParams.has(p)) { url.searchParams.delete(p); stripped++; }
        });
        for (const key of [...url.searchParams.keys()])
            if (key.startsWith('utm_')) { url.searchParams.delete(key); stripped++; }
        stats.strippedParams += stripped;
        return stripped > 0 ? url.toString() : urlStr;
    }

    (function () {
        const c = cleanURL(window.location.href);
        if (c !== window.location.href) history.replaceState(null, '', c);
    })();

    /* ----------------------------------------------------------
     *  LAYER 2  CANVAS / WEBGL / AUDIO / NAVIGATOR SPOOFING
     * ---------------------------------------------------------- */
    (function () {

        try {
            const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
            HTMLCanvasElement.prototype.toDataURL = function (...a) {
                const ctx = this.getContext('2d');
                if (ctx) { const d = ctx.getImageData(0,0,1,1); d.data[3] ^= 1; ctx.putImageData(d,0,0); }
                return origToDataURL.apply(this, a);
            };
        } catch (_) {}

        try {
            const origGID = CanvasRenderingContext2D.prototype.getImageData;
            CanvasRenderingContext2D.prototype.getImageData = function (...a) {
                const d = origGID.apply(this, a); d.data[3] ^= 1; return d;
            };
        } catch (_) {}

        try {
            const patchGL = proto => {
                const orig = proto.getParameter;
                proto.getParameter = function (p) {
                    if (p === 37445) return 'Intel Inc.';
                    if (p === 37446) return 'Intel Iris OpenGL';
                    return orig.call(this, p);
                };
            };
            patchGL(WebGLRenderingContext.prototype);
            if (typeof WebGL2RenderingContext !== 'undefined')
                patchGL(WebGL2RenderingContext.prototype);
        } catch (_) {}

        try {
            if (typeof AudioBuffer !== 'undefined') {
                const orig = AudioBuffer.prototype.getChannelData;
                AudioBuffer.prototype.getChannelData = function (...a) {
                    const d = orig.apply(this, a);
                    if (d.length > 0) d[0] += 0.0000001 * (Math.random() - 0.5);
                    return d;
                };
            }
        } catch (_) {}

        try { safeDefine(Navigator.prototype, 'hardwareConcurrency', 4); } catch (_) {}
        try { safeDefine(Navigator.prototype, 'deviceMemory', 8); } catch (_) {}
        try {
            Object.defineProperty(Navigator.prototype, 'languages', {
                get: () => ['en-GB', 'en'],
                configurable: true,
                enumerable:   true,
            });
        } catch (_) {}

    })();

    /* ----------------------------------------------------------
     *  LAYER 3  NETWORK REQUEST BLOCKING
     * ---------------------------------------------------------- */
    (function () {
        const BLOCKED = [
            /^https?:\/\/([^/]*\.)?google-analytics\.com\//,
            /^https?:\/\/([^/]*\.)?googletagmanager\.com\//,
            /^https?:\/\/([^/]*\.)?googletagservices\.com\//,
            /^https?:\/\/([^/]*\.)?doubleclick\.net\//,
            /^https?:\/\/([^/]*\.)?googlesyndication\.com\/pagead\//,
            /^https?:\/\/([^/]*\.)?facebook\.com\/tr(\/|\?|$)/,
            /^https?:\/\/([^/]*\.)?connect\.facebook\.net\//,
            /^https?:\/\/([^/]*\.)?static\.ads-twitter\.com\//,
            /^https?:\/\/([^/]*\.)?snap\.licdn\.com\//,
            /^https?:\/\/([^/]*\.)?px\.ads\.linkedin\.com\//,
            /^https?:\/\/([^/]*\.)?hotjar\.com\//,
            /^https?:\/\/([^/]*\.)?api\.mixpanel\.com\//,
            /^https?:\/\/([^/]*\.)?api\.amplitude\.com\//,
            /^https?:\/\/([^/]*\.)?api\.segment\.io\//,
            /^https?:\/\/([^/]*\.)?cdn\.segment\.com\//,
            /^https?:\/\/([^/]*\.)?heapanalytics\.com\//,
            /^https?:\/\/([^/]*\.)?fullstory\.com\//,
            /^https?:\/\/([^/]*\.)?static\.getclicky\.com\//,
            /^https?:\/\/([^/]*\.)?quantserve\.com\//,
            /^https?:\/\/([^/]*\.)?scorecardresearch\.com\//,
            /^https?:\/\/([^/]*\.)?trc\.taboola\.com\//,
            /^https?:\/\/([^/]*\.)?outbrain\.com\/paid\//,
        ];
        const isBlocked = url => url && BLOCKED.some(p => p.test(url));

        const origXHR = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function (m, url, ...r) {
            if (isBlocked(url)) { stats.blockedRequests++; return origXHR.call(this, m, 'about:blank', ...r); }
            return origXHR.call(this, m, url, ...r);
        };

        const origFetch = window.fetch;
        window.fetch = function (input, init) {
            const url = typeof input === 'string' ? input
                : (input instanceof Request ? input.url : String(input));
            if (isBlocked(url)) {
                stats.blockedRequests++;
                return Promise.resolve(new Response('', { status: 200 }));
            }
            return origFetch.apply(this, arguments);
        };
    })();

    /* ----------------------------------------------------------
     *  LAYER 4  SEARCH ENGINE LINK CLEANING
     * ---------------------------------------------------------- */
    (function () {
        function unwrap(a) {
            const h = a.href || '';
            if (/\bgoogle\.[a-z.]+\/url\b/.test(h)) {
                try { const u = new URL(h); const d = u.searchParams.get('q') || u.searchParams.get('url'); if (d) { a.href = d; stats.cleanedLinks++; } } catch (_) {}
            } else if (/\bbing\.com\//.test(h)) {
                const r = a.getAttribute('data-href'); if (r) { a.href = r; stats.cleanedLinks++; }
            } else if (/\bsearch\.yahoo\.com\//.test(h)) {
                try { const u = new URL(h); const d = u.searchParams.get('url'); if (d) { a.href = decodeURIComponent(d); stats.cleanedLinks++; } } catch (_) {}
            } else if (/\bduckduckgo\.com\//.test(h)) {
                const c = cleanURL(h); if (c !== h) { a.href = c; stats.cleanedLinks++; }
            }
        }
        document.addEventListener('mousedown', e => { const a = e.target.closest('a[href]'); if (a) unwrap(a); }, true);
        document.addEventListener('DOMContentLoaded', () => document.querySelectorAll('a[href]').forEach(unwrap));
    })();

    /* ----------------------------------------------------------
     *  LAYER 5  CLIENTRECTS SPOOFING
     * ---------------------------------------------------------- */
    (function () {
        const SEED = Math.random() * 1000;
        const MAG  = 0.1;

        function noisyRect(rect) {
            const n = noise(SEED + rect.left + rect.top, MAG);
            return {
                top: rect.top+n, bottom: rect.bottom+n, left: rect.left+n,
                right: rect.right+n, width: rect.width, height: rect.height,
                x: rect.x+n, y: rect.y+n, toJSON: () => ({}),
            };
        }

        function isProbe(el) {
            const s = el.style;
            if (s) {
                const w = parseFloat(s.width), h = parseFloat(s.height);
                if (!isNaN(w) && !isNaN(h) && w < 4 && h < 4) return true;
                const left = parseFloat(s.left), top = parseFloat(s.top);
                if (!isNaN(left) && (left < -100 || left > 9000)) return true;
                if (!isNaN(top)  && (top  < -100 || top  > 9000)) return true;
            }
            if (!document.documentElement.contains(el)) return true;
            return false;
        }

        const origBCR = Element.prototype.getBoundingClientRect;
        Element.prototype.getBoundingClientRect = function () {
            const r = origBCR.call(this);
            if (!isProbe(this)) return r;
            stats.spoofedRects++; return noisyRect(r);
        };

        const origGCR = Element.prototype.getClientRects;
        Element.prototype.getClientRects = function () {
            const r = origGCR.call(this);
            if (!isProbe(this)) return r;
            stats.spoofedRects++;
            const n = Array.from(r).map(noisyRect); n.item = i => n[i]; return n;
        };

        const origRBCR = Range.prototype.getBoundingClientRect;
        Range.prototype.getBoundingClientRect = function () { return noisyRect(origRBCR.call(this)); };

        const origRCR = Range.prototype.getClientRects;
        Range.prototype.getClientRects = function () {
            const r = Array.from(origRCR.call(this)).map(noisyRect); r.item = i => r[i]; return r;
        };
    })();

    /* ----------------------------------------------------------
     *  LAYER 6  FONT FINGERPRINTING
     * ---------------------------------------------------------- */
    (function () {
        const SEED = Math.random() * 9999;
        const MAG  = 0.5;
        const orig = CanvasRenderingContext2D.prototype.measureText;
        CanvasRenderingContext2D.prototype.measureText = function (text) {
            const m = orig.call(this, text);
            const n = noise(SEED + text.length, MAG);
            return new Proxy(m, {
                get(t, p) {
                    if (p === 'width') return t.width + n;
                    const v = t[p]; return typeof v === 'function' ? v.bind(t) : v;
                },
            });
        };
    })();

    /* ----------------------------------------------------------
     *  LAYER 7  WEBGPU FINGERPRINTING
     * ---------------------------------------------------------- */
    (function () {
        if (!navigator.gpu) return;
        const origReq = navigator.gpu.requestAdapter.bind(navigator.gpu);
        navigator.gpu.requestAdapter = async function (...args) {
            const adapter = await origReq(...args);
            if (!adapter) return adapter;
            const masked = { vendor: 'generic', architecture: 'unknown', device: '', description: '' };
            adapter.requestAdapterInfo = async () => masked;
            try { Object.defineProperty(adapter, 'info', { get: () => masked, configurable: true }); } catch (_) {}
            return adapter;
        };
    })();

    /* ----------------------------------------------------------
     *  LAYER 8  WEBRTC HOST CANDIDATE FILTERING
     * ---------------------------------------------------------- */
    (function () {
        if (typeof RTCPeerConnection === 'undefined') return;
        const Orig = window.RTCPeerConnection;

        function Safe(config, constraints) {
            const pc = new Orig(config, constraints);
            const origAEL = pc.addEventListener.bind(pc);
            pc.addEventListener = function (type, handler, ...rest) {
                if (type === 'icecandidate') {
                    const wrapped = function (e) {
                        if (e.candidate && /typ host/.test(e.candidate.candidate || '')) {
                            stats.blockedRTCLeaks++; return;
                        }
                        handler.call(this, e);
                    };
                    return origAEL(type, wrapped, ...rest);
                }
                return origAEL(type, handler, ...rest);
            };
            return pc;
        }

        Safe.prototype = Orig.prototype;
        Object.setPrototypeOf(Safe, Orig);
        try { window.RTCPeerConnection = Safe; } catch (_) {}
    })();

    /* ----------------------------------------------------------
     *  LAYER 9  CLIENT HINTS / USERAGENTDATA
     * ---------------------------------------------------------- */
    (function () {
        if (!navigator.userAgentData) return;
        const BRANDS = [
            { brand: 'Chromium',    version: '120' },
            { brand: 'Not=A?Brand', version:  '24' },
        ];
        const fake = {
            brands: BRANDS, mobile: false, platform: 'Windows',
            getHighEntropyValues: async () => ({
                brands: BRANDS, mobile: false, platform: 'Windows',
                platformVersion: '10.0.0', architecture: 'x86', bitness: '64',
                model: '', uaFullVersion: '120.0.0.0',
                fullVersionList: BRANDS.map(b => ({ brand: b.brand, version: b.version + '.0.0.0' })),
            }),
            toJSON() { return { brands: BRANDS, mobile: false, platform: 'Windows' }; },
        };
        try {
            Object.defineProperty(Navigator.prototype, 'userAgentData', {
                get: () => fake, configurable: true,
            });
        } catch (_) {}
    })();

    /* ----------------------------------------------------------
     *  LAYER 10  TIMEZONE & INTL SPOOFING  (FIXED v2.7)
     *
     *  ROOT CAUSE OF v2.6 BREAKAGE:
     *  FakeDTF was forcing FAKE_LOCALE = 'en-US' on every
     *  Intl.DateTimeFormat call globally, ignoring what the
     *  site passed. This broke:
     *    - Non-English sites rendering dates/times
     *    - Currency formatting (e.g. showing $ instead of €)
     *    - Number formatting with locale-specific separators
     *    - Any site using Intl for i18n
     *  Similarly, toLocaleString was overriding locale to
     *  en-US unconditionally, breaking the same things.
     *
     *  THE FIX:
     *  - Caller's locale is always passed through unchanged.
     *  - Only timeZone is injected / overridden.
     *  - resolvedOptions() still returns the real locale the
     *    formatter was created with — only timeZone is patched.
     *  - toLocaleString family only injects timeZone into
     *    options, never touches the locale argument.
     * ---------------------------------------------------------- */
    (function () {
        const FAKE_TZ        = 'America/New_York';
        const FAKE_TZ_OFFSET = 300; // UTC-5 (America/New_York standard time)

        // ── Date.getTimezoneOffset() ─────────────────────────
        try {
            Date.prototype.getTimezoneOffset = function () {
                return FAKE_TZ_OFFSET;
            };
        } catch (_) {}

        // ── Intl.DateTimeFormat ──────────────────────────────
        // Only spoof timeZone. Pass caller's locale through as-is.
        try {
            const OrigDTF = Intl.DateTimeFormat;

            function FakeDTF(locale, options) {
                // Preserve whatever locale the caller passed —
                // only inject our fake timezone into options.
                const safeOptions = Object.assign({}, options || {}, { timeZone: FAKE_TZ });

                const instance = new OrigDTF(locale, safeOptions);

                // Patch resolvedOptions to report fake timezone only.
                // All other fields (locale, calendar, etc.) are real.
                const origResolved = instance.resolvedOptions.bind(instance);
                instance.resolvedOptions = function () {
                    const real = origResolved();
                    return Object.assign({}, real, { timeZone: FAKE_TZ });
                };

                return instance;
            }

            FakeDTF.prototype          = OrigDTF.prototype;
            FakeDTF.supportedLocalesOf = OrigDTF.supportedLocalesOf;
            Object.setPrototypeOf(FakeDTF, OrigDTF);

            try { Intl.DateTimeFormat = FakeDTF; } catch (_) {}

        } catch (_) {}

        // ── Date toLocale* methods ───────────────────────────
        // Only inject timeZone. Never touch the locale argument.
        ['toLocaleString', 'toLocaleDateString', 'toLocaleTimeString'].forEach(method => {
            try {
                const orig = Date.prototype[method];
                Date.prototype[method] = function (locale, options) {
                    // Pass locale through unchanged —
                    // only add timeZone to the options object.
                    const safeOptions = Object.assign({}, options || {}, { timeZone: FAKE_TZ });
                    return orig.call(this, locale, safeOptions);
                };
            } catch (_) {}
        });

    })();

    /* ----------------------------------------------------------
     *  LAYER 11  NETWORK INFORMATION API SPOOFING
     * ---------------------------------------------------------- */
    (function () {
        try {
            const fakeConnection = {
                type:          'wifi',
                effectiveType: '4g',
                downlink:      10,
                downlinkMax:   Infinity,
                rtt:           50,
                saveData:      false,
                onchange:      null,
                addEventListener:    () => {},
                removeEventListener: () => {},
                dispatchEvent:       () => true,
            };

            Object.defineProperty(Navigator.prototype, 'connection', {
                get: () => fakeConnection, configurable: true, enumerable: true,
            });
            Object.defineProperty(Navigator.prototype, 'mozConnection', {
                get: () => fakeConnection, configurable: true,
            });
            Object.defineProperty(Navigator.prototype, 'webkitConnection', {
                get: () => fakeConnection, configurable: true,
            });
        } catch (_) {}
    })();

    /* ----------------------------------------------------------
     *  LAYER 12  SPEECHSYNTHESIS SPOOFING  (FIXED v2.7)
     *
     *  ROOT CAUSE OF v2.6 BREAKAGE:
     *  Blocking the voiceschanged event caused any site that
     *  loads TTS voices asynchronously to hang forever waiting
     *  for the event that signals voices are ready to use.
     *  The event block was overly aggressive — fingerprinters
     *  can still be defeated by simply returning an empty list
     *  from getVoices() without touching the event system.
     *
     *  THE FIX:
     *  - voiceschanged event listener blocking removed entirely.
     *  - getVoices() still returns [] so voice enumeration
     *    fingerprinting is defeated.
     *  - Sites using TTS legitimately can still detect when
     *    the API is ready; they just get no voices back.
     * ---------------------------------------------------------- */
    (function () {
        try {
            if (typeof SpeechSynthesis === 'undefined') return;

            // Return empty voice list — prevents voice enumeration
            // fingerprinting without blocking the event system.
            SpeechSynthesis.prototype.getVoices = function () {
                return [];
            };

        } catch (_) {}
    })();

    /* ----------------------------------------------------------
     *  END OF PRIVACY SHIELD v2.7
     * ---------------------------------------------------------- */

})();