Hide Referrer

Hide HTTP Referrer on all websites to prevent destination sites from tracking your origin page

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Hide Referrer
// @namespace    https://github.com/planetoid/userscripts
// @version      1.0.0
// @description  Hide HTTP Referrer on all websites to prevent destination sites from tracking your origin page
// @author       Planetoid
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ============================================================
    // Configuration
    // ============================================================

    const CONFIG = {
        // Whether to inject <meta> referrer tag (primary protection)
        injectMetaTag: true,

        // Whether to add rel="noreferrer" to all <a> tags
        patchLinks: true,

        // Whether to intercept window.open so it doesn't send referrer
        patchWindowOpen: true,

        // Whether to observe DOM changes and patch dynamically added links (for SPAs)
        observeDom: true,

        // Excluded domains (referrer hiding will be skipped on these sites)
        // e.g. ['example.com', 'mysite.org']
        excludedDomains: [],

        // Whether to show debug messages in the console
        debug: false,
    };

    // ============================================================
    // Utilities
    // ============================================================

    function log(...args) {
        if (CONFIG.debug) {
            console.log('[Hide Referrer]', ...args);
        }
    }

    function isExcluded() {
        return CONFIG.excludedDomains.some(
            (domain) =>
                location.hostname === domain ||
                location.hostname.endsWith('.' + domain)
        );
    }

    function isExternalLink(anchor) {
        try {
            return anchor.href && new URL(anchor.href).hostname !== location.hostname;
        } catch {
            return false;
        }
    }

    // ============================================================
    // 1. Inject <meta name="referrer" content="no-referrer">
    //    Injected as early as possible during document-start
    // ============================================================

    function injectMetaTag() {
        if (!CONFIG.injectMetaTag) return;

        const meta = document.createElement('meta');
        meta.name = 'referrer';
        meta.content = 'no-referrer';

        // <head> may not exist yet at document-start, try multiple mount points
        const parent =
            document.head ||
            document.documentElement ||
            document.querySelector('head');

        if (parent) {
            parent.insertBefore(meta, parent.firstChild);
            log('Meta tag injected into', parent.tagName);
        } else {
            // Very early stage — wait for the DOM to appear before injecting
            const observer = new MutationObserver(() => {
                const target = document.head || document.documentElement;
                if (target) {
                    target.insertBefore(meta, target.firstChild);
                    log('Meta tag injected (deferred)');
                    observer.disconnect();
                }
            });
            observer.observe(document, { childList: true, subtree: true });
        }
    }

    // ============================================================
    // 2. Add rel="noreferrer noopener" to all <a> tags
    // ============================================================

    function patchAnchor(anchor) {
        if (!anchor.href || anchor.dataset.referrerPatched) return;

        // Only patch external links — remove this check to patch all links
        if (!isExternalLink(anchor)) return;

        const relValues = new Set((anchor.rel || '').split(/\s+/).filter(Boolean));
        relValues.add('noreferrer');
        relValues.add('noopener');
        anchor.rel = [...relValues].join(' ');
        anchor.dataset.referrerPatched = '1';

        log('Patched link:', anchor.href);
    }

    function patchAllLinks() {
        if (!CONFIG.patchLinks) return;
        document.querySelectorAll('a[href]').forEach(patchAnchor);
    }

    // ============================================================
    // 3. Intercept window.open — use an intermediate page to strip referrer
    // ============================================================

    function patchWindowOpen() {
        if (!CONFIG.patchWindowOpen) return;

        const originalOpen = window.open;

        window.open = function (url, target, features) {
            if (url) {
                try {
                    const parsed = new URL(url, location.href);
                    if (parsed.hostname !== location.hostname) {
                        // Use a Blob URL as an intermediate page to strip referrer
                        const html = `
              <!DOCTYPE html>
              <html>
              <head>
                <meta name="referrer" content="no-referrer">
                <meta http-equiv="refresh" content="0;url=${parsed.href}">
              </head>
              <body></body>
              </html>`;
                        const blob = new Blob([html], { type: 'text/html' });
                        const blobUrl = URL.createObjectURL(blob);
                        const win = originalOpen.call(window, blobUrl, target, features);

                        // Revoke Blob URL after a short delay
                        setTimeout(() => URL.revokeObjectURL(blobUrl), 3000);
                        log('window.open intercepted:', url);
                        return win;
                    }
                } catch (e) {
                    log('window.open parse error:', e);
                }
            }
            return originalOpen.call(window, url, target, features);
        };

        log('window.open overridden');
    }

    // ============================================================
    // 4. MutationObserver — watch for dynamically added links
    // ============================================================

    function observeDom() {
        if (!CONFIG.observeDom || !CONFIG.patchLinks) return;

        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== Node.ELEMENT_NODE) continue;

                    // If the added node itself is an <a>
                    if (node.tagName === 'A') {
                        patchAnchor(node);
                    }

                    // Check the subtree of the added node for <a> elements
                    if (node.querySelectorAll) {
                        node.querySelectorAll('a[href]').forEach(patchAnchor);
                    }
                }
            }
        });

        // Wait for <body> to exist before observing
        function startObserving() {
            if (document.body) {
                observer.observe(document.body, { childList: true, subtree: true });
                log('DOM Observer started');
            } else {
                requestAnimationFrame(startObserving);
            }
        }

        startObserving();
    }

    // ============================================================
    // Main
    // ============================================================

    if (isExcluded()) {
        log('Current domain is excluded, skipping');
        return;
    }

    // Phase 1: document-start (as early as possible)
    injectMetaTag();
    patchWindowOpen();

    // Phase 2: Patch links after DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            patchAllLinks();
            observeDom();
        });
    } else {
        patchAllLinks();
        observeDom();
    }

    log('Hide Referrer userscript loaded');
})();