Referrer Blocking Chrome Extension (2026)
Chrome Referrer Blocking Extension: A Developer’s Guide
The HTTP Referrer header has been a staple of web analytics since the early days of the internet. However, it also poses significant privacy concerns and can leak sensitive URL information across domains. For developers and power users, understanding how to control or block the Referrer header in Chrome becomes essential for building privacy-respecting applications and protecting user data.
This guide covers the technical implementation of referrer blocking in Chrome extensions, providing practical code examples and real-world use cases. By the end, you will have a complete, deployable extension and a solid understanding of when and why to apply different referrer policies.
Understanding the Referrer Header
When you click a link on website A that takes you to website B, the browser automatically sends a Referer header (note the historical misspelling) indicating where the user came from. This header can contain:
- Full URLs including query parameters
- Path information that may reveal user-specific data
- Fragment identifiers with sensitive identifiers
- Session tokens embedded in URLs by legacy systems
GET /landing-page HTTP/1.1
Host: example.com
Referer: https://previous-site.com/user/profile?id=12345&token=abc
That request exposes the user’s ID and a token to the destination server. The destination server logs this, third-party analytics scripts on that page log it, and if the page is served over HTTP, every network hop between the client and server sees it.
Real-World Leakage Scenarios
Consider these practical situations where referrer leakage causes real problems:
Healthcare dashboards. A hospital patient portal includes the patient’s record ID in its URL. When a patient clicks an external link on that page (say, a link to a drug information website), the drug site now knows that record ID came from the hospital’s domain.
Internal tooling. A developer working inside a corporate intranet clicks a StackOverflow link from an internal ticket tracker. The full internal URL. including ticket numbers, project names, and any embedded tokens. goes to StackOverflow.
E-commerce checkout flows. A checkout page URL often contains cart identifiers or promo codes. Clicking any outbound link from that page leaks those details to the destination.
SaaS admin panels. Admin pages typically include resource identifiers in the URL. Clicking documentation links from an admin page can reveal your infrastructure topology to the docs provider.
Chrome extensions can intercept and modify this behavior using the declarativeNetRequest API, which provides a performant way to modify network requests without requiring broad host permissions or heavyweight content scripts.
Referrer Policy Values: What Your Options Are
Before building an extension, understand the full spectrum of Referrer-Policy values available. This table summarizes each value and when to use it:
| Policy | What gets sent | Best for |
|---|---|---|
no-referrer |
Nothing | Maximum privacy, no analytics needed |
no-referrer-when-downgrade |
Full URL on HTTPS→HTTPS, nothing on HTTPS→HTTP | Default browser behavior |
origin |
Scheme + host only (https://mysite.com) |
Useful for analytics without path exposure |
origin-when-cross-origin |
Full URL same-origin, origin only cross-origin | Balanced: internal detail kept internal |
same-origin |
Full URL same-origin, nothing cross-origin | Good default for sensitive apps |
strict-origin |
Origin on HTTPS→HTTPS, nothing on HTTPS→HTTP | Like origin but blocks HTTP downgrade |
strict-origin-when-cross-origin |
Full URL same-origin, strict-origin cross-origin | Chrome’s current default since 2021 |
unsafe-url |
Full URL always | Debugging only, never in production |
For most privacy-focused extensions, no-referrer or same-origin are the right choices.
Setting Up Your Extension
Create a new Chrome extension project with the following structure:
referrer-blocker/
manifest.json
rules.json
background.js
content-script.js
popup.html
popup.js
Manifest Configuration
Your manifest must declare the declarativeNetRequest permission:
{
"manifest_version": 3,
"name": "Referrer Blocker",
"version": "1.0",
"description": "Block or control the HTTP Referrer header for all requests",
"permissions": [
"declarativeNetRequest",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_title": "Referrer Blocker"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"run_at": "document_end"
}
]
}
The storage permission lets you persist the user’s enabled/disabled state across browser sessions.
Implementing Referrer Blocking Rules
The declarativeNetRequest API uses JSON rules to define how headers should be modified. Rules are evaluated in the browser’s network stack. not in JavaScript. which makes them fast and reliable.
Basic Referrer Removal
// background.js
chrome.runtime.onInstalled.addListener(() => {
const rules = [
{
id: 1,
priority: 1,
action: {
type: "modifyHeaders",
requestHeaders: [
{ header: "Referer", operation: "set", value: "" }
]
},
condition: {
urlFilter: "*://*/*",
resourceTypes: ["main_frame", "sub_frame"]
}
}
];
chrome.declarativeNetRequest.updateDynamicRules({
addRules: rules,
removeRuleIds: []
});
});
This rule removes the Referrer header entirely for all main frame and iframe navigations. The set operation with an empty string value causes the header to be sent with no content. Some servers treat an empty string differently from an absent header. if you want the header completely absent, check whether the Chrome version you are targeting supports the remove operation instead.
Selective Referrer Blocking
You should block referrers only for specific domains or allow them for trusted sites:
// background.js
const rules = [
// Block referrer when leaving your domain
{
id: 1,
priority: 1,
action: {
type: "modifyHeaders",
requestHeaders: [
{ header: "Referer", operation: "set", value: "" }
]
},
condition: {
urlFilter: "*://*/*",
resourceTypes: ["main_frame"],
initiatorDomains: ["yourdomain.com"]
}
},
// Allow referrer to specific analytics domains
{
id: 2,
priority: 2,
action: {
type: "allow"
},
condition: {
urlFilter: "*://analytics.example.com/*",
resourceTypes: ["image", "script"]
}
}
];
chrome.declarativeNetRequest.updateDynamicRules({
addRules: rules,
removeRuleIds: []
});
Rule priority matters here. A higher priority number wins when two rules match the same request. In the example above, any request to analytics.example.com is allowed (priority 2 beats priority 1) while everything else from your domain gets stripped.
Domain Allowlist Pattern
For enterprise deployments where you need to whitelist a set of trusted domains, structure your rules as an array and register them together:
// background.js
const TRUSTED_DOMAINS = [
"internalanalytics.corp.com",
"staging.myapp.com",
"docs.myapp.com"
];
function buildAllowRules(domains) {
return domains.map((domain, index) => ({
id: 100 + index,
priority: 3, // Higher than blocking rule
action: { type: "allow" },
condition: {
urlFilter: `*://${domain}/*`,
resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest"]
}
}));
}
function buildBlockRule() {
return {
id: 1,
priority: 1,
action: {
type: "modifyHeaders",
requestHeaders: [
{ header: "Referer", operation: "set", value: "" }
]
},
condition: {
urlFilter: "*://*/*",
resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest"]
}
};
}
chrome.runtime.onInstalled.addListener(async () => {
const allRules = [buildBlockRule(), ...buildAllowRules(TRUSTED_DOMAINS)];
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: allRules,
removeRuleIds: []
});
console.log(`Loaded ${allRules.length} referrer rules`);
});
Using referrerPolicy on Links
Modern browsers support the referrerPolicy attribute directly on anchor elements. You can inject this attribute into all links via a content script, providing a second layer of protection that works even when header-level rules are not active:
<!-- Block referrer entirely -->
<a href="https://example.com" referrerpolicy="no-referrer">Link</a>
<!-- Send only origin, not full URL -->
<a href="https://example.com/page?id=123" referrerpolicy="origin">Link</a>
<!-- Same-origin only -->
<a href="/internal-page" referrerpolicy="same-origin">Link</a>
For a Chrome extension, inject this attribute into all links automatically:
// content-script.js
function applyReferrerPolicy(policy = 'no-referrer') {
const links = document.querySelectorAll('a[href]');
let patched = 0;
links.forEach(link => {
if (!link.hasAttribute('referrerpolicy')) {
link.setAttribute('referrerpolicy', policy);
patched++;
}
});
return patched;
}
// Apply on initial load
document.addEventListener('DOMContentLoaded', () => {
const count = applyReferrerPolicy();
console.log(`[ReferrerBlocker] Patched ${count} links`);
});
// Watch for dynamically added links (SPAs)
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const links = node.querySelectorAll
? node.querySelectorAll('a[href]')
: [];
links.forEach(link => {
if (!link.hasAttribute('referrerpolicy')) {
link.setAttribute('referrerpolicy', 'no-referrer');
}
});
// Handle the node itself if it's a link
if (node.tagName === 'A' && !node.hasAttribute('referrerpolicy')) {
node.setAttribute('referrerpolicy', 'no-referrer');
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
The MutationObserver is critical for single-page applications (React, Vue, Angular) where links are injected after the initial DOM load.
Building a Complete Extension with Toggle UI
Here’s a complete implementation that includes a popup UI for toggling blocking on and off, with state persisted to storage:
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { width: 240px; padding: 16px; font-family: system-ui, sans-serif; }
h2 { margin: 0 0 12px; font-size: 14px; }
.status { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
.indicator { width: 10px; height: 10px; border-radius: 50%; }
.indicator.on { background: #22c55e; }
.indicator.off { background: #ef4444; }
button { width: 100%; padding: 8px; cursor: pointer; border-radius: 4px; border: 1px solid #ccc; }
</style>
</head>
<body>
<h2>Referrer Blocker</h2>
<div class="status">
<div id="indicator" class="indicator off"></div>
<span id="status-text">Loading...</span>
</div>
<button id="toggle">Toggle</button>
<script src="popup.js"></script>
</body>
</html>
// popup.js
async function getState() {
const result = await chrome.storage.local.get({ enabled: true });
return result.enabled;
}
async function setState(enabled) {
await chrome.storage.local.set({ enabled });
}
async function updateUI(enabled) {
const indicator = document.getElementById('indicator');
const statusText = document.getElementById('status-text');
indicator.className = `indicator ${enabled ? 'on' : 'off'}`;
statusText.textContent = enabled ? 'Blocking active' : 'Blocking disabled';
}
document.addEventListener('DOMContentLoaded', async () => {
const enabled = await getState();
await updateUI(enabled);
document.getElementById('toggle').addEventListener('click', async () => {
const current = await getState();
const next = !current;
await setState(next);
await updateUI(next);
// Notify background to update rules
chrome.runtime.sendMessage({ type: 'SET_BLOCKING', enabled: next });
});
});
// background.js (complete version)
const BLOCK_RULE_ID = 1;
function buildBlockRule() {
return {
id: BLOCK_RULE_ID,
priority: 1,
action: {
type: "modifyHeaders",
requestHeaders: [
{ header: "Referer", operation: "set", value: "" }
]
},
condition: {
urlFilter: "*://*/*",
resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest", "script", "image"]
}
};
}
async function enableBlocking() {
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [buildBlockRule()],
removeRuleIds: [BLOCK_RULE_ID]
});
}
async function disableBlocking() {
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [],
removeRuleIds: [BLOCK_RULE_ID]
});
}
chrome.runtime.onInstalled.addListener(async () => {
const result = await chrome.storage.local.get({ enabled: true });
if (result.enabled) {
await enableBlocking();
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'SET_BLOCKING') {
const action = message.enabled ? enableBlocking() : disableBlocking();
action.then(() => sendResponse({ ok: true }));
return true;
}
});
Testing Your Extension
Load your extension in Chrome by navigating to chrome://extensions/, enabling Developer mode, and clicking “Load unpacked”. Test the following scenarios:
- Click links on various websites and verify no referrer is sent. check the Network tab in DevTools and inspect request headers
- Confirm that the
Refererheader is absent or empty in outbound requests - Test with different resource types: images, scripts, XHR calls, and fetch requests
- Navigate from a URL with query parameters to an external site and confirm those parameters do not appear in the destination’s logs
- Toggle the extension off and verify referrers resume being sent
- Test SPA navigation on React/Vue apps to confirm the MutationObserver picks up dynamically added links
Use chrome://net-internals/#events for lower-level request inspection when DevTools does not show the headers you expect.
Debugging Common Issues
Rules not applying. Double-check your host_permissions in the manifest. Without <all_urls> or the specific domain you are targeting, rules will silently fail to match.
Server-set Referrer-Policy overrides your extension. When a server sends a Referrer-Policy response header with a restrictive value, that takes precedence over what the browser would otherwise send. but your extension’s header modification happens before the browser processes response headers, so your rule still wins for outbound requests. The response header governs what subsequent navigations from that page send.
Empty string vs. absent header. Some web applications behave differently when Referer: (empty) is present versus when it is absent entirely. If you see unexpected behavior, try using operation: "remove" (available in Chrome 102+) instead of set with an empty value.
Iframe content. Sub-frame navigations require explicit inclusion in your rule’s resourceTypes array. The sub_frame type covers iframes; omitting it means nested frames still leak referrers.
Performance Considerations
The declarativeNetRequest API runs efficiently because:
- Rules are evaluated in the browser’s network stack, not JavaScript
- No content scripts are required for basic header modification
- Rules can be updated dynamically without reloading the extension
- The rule evaluation is O(1) per request. it does not scale with the number of rules the way older approaches did
For extensions with complex rule sets, consider using static rule sets stored in rules.json declared in the manifest. Static rules are compiled at install time and are marginally faster than dynamic rules, but dynamic rules are flexible enough for most use cases.
Comparison: Extension Approaches
| Approach | Performance | Flexibility | Privacy |
|---|---|---|---|
declarativeNetRequest header rule |
Excellent | Medium | Excellent |
| Content script link patching | Good | High | Good (misses non-link requests) |
Referrer-Policy meta tag injection |
Fast | Low | Partial (no control over existing tags) |
webRequest API (MV2 only) |
Slower | Very high | Excellent |
For Manifest V3, the declarativeNetRequest approach combined with content script link patching gives you the best coverage.
Conclusion
Building a Chrome referrer blocking extension requires understanding the declarativeNetRequest API, proper manifest configuration, and thoughtful rule design. The examples provided here give you a foundation for creating privacy-respecting extensions that protect user data without breaking legitimate use cases.
For production deployments, consider adding user controls for different blocking modes (no-referrer vs. origin vs. same-origin), a logging panel for debugging, and compatibility testing across different Chrome versions. The combination of header-level rules and content script link patching provides defense in depth that handles both standard navigation and dynamically generated content in modern SPAs.
Try it: Paste your error into our Error Diagnostic for an instant fix.
Related Reading
- AI Calendar Assistant Chrome Extension: A Developer’s Guide
- AI Form Filler Chrome Extension: A Developer and Power.
- AI Podcast Summary Chrome Extension: A Developer’s Guide.
Built by theluckystrike. More at zovo.one
Find the right skill → Browse 155+ skills in our Skill Finder.