CORS Implementation Methods - Comparison
Different Approaches to CORS Override in Chrome Extensions
There are 3 main approaches to implementing CORS override in Chrome extensions, each with different trade-offs:
1. webRequest API (Manifest V2 - LEGACY) 🔴
How Moesif CORS Extension Works
Implementation:
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
details.responseHeaders.push({
name: "Access-Control-Allow-Origin",
value: "*"
});
details.responseHeaders.push({
name: "Access-Control-Allow-Methods",
value: "*"
});
details.responseHeaders.push({
name: "Access-Control-Allow-Headers",
value: "*"
});
return { responseHeaders: details.responseHeaders };
},
{ urls: ["<all_urls>"] },
["blocking", "responseHeaders", "extraHeaders"]
);
Pros ✅
- Very flexible - Can inspect request/response and modify headers dynamically
- Can read response bodies - Full access to network traffic
- Can set exact origin - Can do
Access-Control-Allow-Origin: https://example.com (enables credentials)
- Can append headers - Doesn’t have to replace entire header
- Can handle CORS preflight - With
extraHeaders flag
- Fine-grained control - Can modify based on request content, timing, etc.
- Works with credentials - Can set exact origin + credentials=true
Cons ❌
- MANIFEST V2 ONLY - Deprecated, will be removed in 2025+
- Blocking listeners banned in MV3 - Only available to enterprise/policy-installed extensions
- Performance impact - Intercepts EVERY network request, even when inactive
- Privacy concerns - Extension can read all network traffic (Chrome warns users)
- Requires blocking permission - Triggers scary permission warning
- Not future-proof - Google is actively deprecating this API
Status: 🔴 DEPRECATED - DO NOT USE FOR NEW EXTENSIONS
2. declarativeNetRequest API (Manifest V3 - OUR CURRENT APPROACH) 🟢
How EchoKit Implements CORS
Implementation:
chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 1001,
priority: 1,
action: {
type: 'modifyHeaders',
responseHeaders: [
{ header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' },
{ header: 'Access-Control-Allow-Methods', operation: 'set', value: '*' },
{ header: 'Access-Control-Allow-Headers', operation: 'set', value: '*' }
]
},
condition: {
urlFilter: '|http',
resourceTypes: ['xmlhttprequest', 'main_frame', ...]
}
}]
});
Pros ✅
- Manifest V3 compatible - Future-proof, won’t be deprecated
- Zero performance overhead - Rules are declarative, no JS execution per request
- Privacy-friendly - Extension can’t read network traffic
- No scary permissions - Doesn’t require webRequestBlocking
- Rules persist - Dynamic rules survive browser restarts (global scope)
- Tab/domain scoping - Session rules support
tabIds and requestDomains filters
- Efficient - Chrome engine handles rules natively
Cons ❌
- Cannot set exact origin - Can only use wildcard
* (no credentials support)
- Cannot read request/response - No access to body or dynamic header inspection
- Cannot append headers - Must replace entire header with static value
- Limited flexibility - Rules are declarative, can’t run custom logic
- No conditional logic - Can’t modify based on request content
- CORS credentials broken -
Access-Control-Allow-Origin: * incompatible with credentials
Status: 🟢 RECOMMENDED FOR MV3 EXTENSIONS
3. Hybrid: declarativeNetRequest + webRequest (Non-blocking) 🟡
How Some Extensions Work Around MV3 Limitations
Implementation:
// Use DNR for most CORS cases (fast, declarative)
chrome.declarativeNetRequest.updateDynamicRules({...});
// Use non-blocking webRequest for observation/analytics ONLY
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
// Can READ headers but CANNOT modify them (no 'blocking')
console.log('Request to:', details.url);
console.log('Response headers:', details.responseHeaders);
},
{ urls: ["<all_urls>"] },
["responseHeaders"] // NO 'blocking' - observation only
);
Pros ✅
- MV3 compatible - Non-blocking webRequest is allowed
- Can observe traffic - Read headers for analytics/debugging
- Fast CORS override - DNR handles the actual header modification
- Best of both worlds - Declarative performance + observational flexibility
Cons ❌
- Cannot modify in webRequest - Observation only, DNR does the real work
- Still can’t support credentials - DNR limitation remains
- More complex - Two APIs to maintain
- Limited benefit - Non-blocking webRequest can’t fix DNR’s limitations
Status: 🟡 USEFUL FOR ANALYTICS, DOESN’T SOLVE CREDENTIALS ISSUE
Comparison Table
| Feature |
webRequest (MV2) |
declarativeNetRequest (MV3) |
Our Implementation |
| Manifest V3 Compatible |
❌ No |
✅ Yes |
✅ Yes |
| Future-proof |
❌ Deprecated 2025+ |
✅ Yes |
✅ Yes |
| Performance |
❌ Slow (intercepts all) |
✅ Fast (declarative) |
✅ Fast |
| Privacy-friendly |
❌ Reads all traffic |
✅ No traffic access |
✅ No traffic access |
| Scary permissions |
❌ Yes (webRequestBlocking) |
✅ No |
✅ No |
| CORS credentials support |
✅ Yes (exact origin) |
❌ No (wildcard only) |
❌ No |
| Tab/Domain scoping |
✅ Yes (custom logic) |
✅ Yes (session rules) |
✅ Yes |
| Dynamic header values |
✅ Yes |
❌ No (static only) |
❌ No |
| Read response body |
✅ Yes |
❌ No |
❌ No |
| Zero overhead when off |
❌ No |
✅ Yes |
✅ Yes |
| Enterprise only (MV3) |
✅ Yes (blocking) |
❌ No |
❌ No |
Why Moesif CORS Extension Still Works Better
Moesif is still on Manifest V2 using the deprecated webRequest API.
Their advantages:
- Can set exact origin -
Access-Control-Allow-Origin: https://example.com
- Credentials work - Can use
Access-Control-Allow-Credentials: true with exact origin
- Dynamic behavior - Can read request origin and echo it back
- Handles preflight - Full control over OPTIONS requests with
extraHeaders
Their problem:
- Will stop working when Chrome removes MV2 support (targeted 2025-2026)
- Must migrate to MV3 eventually and will face same limitations as us
Should We Switch Approaches?
Option A: Stay with declarativeNetRequest (RECOMMENDED) ✅
Verdict: YES - This is the right approach for MV3
Reasoning:
- Future-proof and won’t be deprecated
- Privacy-friendly (no scary permissions)
- Zero performance overhead
- Wildcard CORS works for 95% of use cases
- Credentials are rarely needed in local development
Trade-off: Can’t support Access-Control-Allow-Credentials: true use cases
Option B: Add Observational webRequest (HYBRID) 🟡
Verdict: MAYBE - Limited benefits for our use case
What we could add:
// Add to background.js
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
// Can only READ headers (non-blocking)
// Log CORS-related info for debugging
const corsHeaders = details.responseHeaders?.filter(h =>
h.name.toLowerCase().startsWith('access-control')
);
console.log('[EchoKit CORS Debug]', details.url, corsHeaders);
},
{ urls: ["<all_urls>"] },
["responseHeaders"] // NO 'blocking' - observation only
);
Benefits:
- Better debugging information
- Can detect when servers reject wildcard CORS
- Can log CORS preflight requests
Downsides:
- Adds complexity
- Doesn’t solve credentials limitation
- Still can’t modify headers (DNR does that)
- Minimal user-facing benefit
Recommendation: NOT WORTH IT - Our diagnostics tool already provides sufficient debugging
Option C: Hybrid with Content Script Injection 🟡
Verdict: POSSIBLE - But very hacky
How it works:
- Use DNR for basic CORS override
- For credentialed requests, inject content script to:
- Intercept fetch/XHR before it’s sent
- Rewrite requests to use a proxy endpoint
- Proxy adds correct origin header server-side
Example:
// Content script injected into page
const originalFetch = window.fetch;
window.fetch = function(url, options) {
if (needsCredentials(options)) {
// Route through extension proxy
return originalFetch('chrome-extension://ID/proxy', {
method: 'POST',
body: JSON.stringify({ url, options })
});
}
return originalFetch(url, options);
};
Benefits:
- Could support credentials
- Transparent to web app
Downsides:
- EXTREMELY HACKY
- Performance overhead
- Security concerns (proxy)
- Breaks preflight caching
- CSP might block injection
- Very complex to maintain
Recommendation: NO - Way too complex for minimal gain
Advanced Techniques Other Extensions Use
1. Per-Site Rule Management (Like AlexBatok/cors-bypass)
What we could adopt:
- Allow users to enable CORS per domain
- Default blocklist for sensitive sites (google.com, github.com)
- Import/export CORS domain lists
Implementation:
// Store per-domain CORS settings
const corsEnabledDomains = new Set(['localhost', 'dev.example.com']);
// Update DNR rules to match only enabled domains
await chrome.declarativeNetRequest.updateSessionRules({
addRules: [{
id: 1001,
action: { type: 'modifyHeaders', ... },
condition: {
requestDomains: Array.from(corsEnabledDomains),
resourceTypes: [...]
}
}]
});
Benefit: More granular control, safer
What we could adopt:
- Let users choose which headers to inject
- Allow custom header values
- Toggle specific headers on/off
Implementation:
// User-configurable CORS settings
const corsConfig = {
allowOrigin: { enabled: true, value: '*' },
allowMethods: { enabled: true, value: 'GET,POST,PUT,DELETE' },
allowHeaders: { enabled: true, value: '*' },
allowCredentials: { enabled: false }, // Can't use with wildcard
exposeHeaders: { enabled: false, value: '' },
maxAge: { enabled: true, value: '86400' }
};
// Build headers array from config
const responseHeaders = Object.entries(corsConfig)
.filter(([k, v]) => v.enabled)
.map(([k, v]) => ({
header: `Access-Control-${pascalCase(k)}`,
operation: 'set',
value: v.value
}));
Benefit: More flexibility for edge cases
3. Debug Panel with Request Modification Count
What we could adopt from AlexBatok:
- Show how many requests were modified
- Show which domains CORS is active on
- Display CORS rule status per tab
UI Enhancement:
CORS Override: ON (Domain scope)
━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Active on: localhost:3000
📊 Requests modified: 47
🔍 Last modified: 2s ago
POST /api/users → 200 OK
Benefit: Better visibility into what’s happening
Recommendations for EchoKit
Short-term Improvements (OPTIONAL) 🟡
- Per-domain CORS toggle (instead of global ON/OFF)
- Let users enable CORS for specific domains
- Default blocklist for sensitive sites
- Prevents accidental breakage
- CORS modification counter
- Show “Modified 47 requests” badge
- Help users confirm CORS is working
- Simple UI enhancement
- Configurable headers
- Let users customize which CORS headers to inject
- Add Access-Control-Expose-Headers option
- Add Access-Control-Max-Age configuration
Long-term (NOT RECOMMENDED) ❌
- Don’t add webRequest blocking (deprecated in MV3)
- Don’t build proxy for credentials (too complex)
- Don’t try to support credentials use case (impossible with DNR)
Final Verdict
Our implementation is CORRECT and OPTIMAL for Manifest V3 ✅
Why:
- Uses the right API (declarativeNetRequest)
- Future-proof (won’t be deprecated)
- Privacy-friendly (no traffic inspection)
- Performance-efficient (declarative rules)
- Scope-aware (tab/domain/global)
- Good diagnostics (debugging support)
Why Moesif appears “better”:
- Still on MV2 (deprecated)
- Uses webRequest blocking (will be removed)
- Has more flexibility NOW
- Will HAVE TO migrate to DNR eventually and face same limitations
Trade-offs we accept:
- ❌ Can’t support
Access-Control-Allow-Credentials: true
- ❌ Can’t set exact origin (wildcard only)
- ❌ Can’t read response bodies
Why these trade-offs are acceptable:
- 95% of CORS issues in local dev don’t need credentials
- Wildcard origin works for most API testing
- Alternative: configure server CORS properly (better solution)
No need to change our approach - We made the right choice! 🎯