Web Privacy Engineering Practices
Beyond consent management platforms, web engineers can implement numerous HTML and technical practices to protect user privacy. These practices prevent data leakage through referrer headers, link tracking, and other technical mechanisms that expose user browsing behavior to third parties.
Table of Contents
- Why HTML-Level Privacy Matters
- Referrer Policy Implementation
- Anchor Link Privacy Attributes
- Form Submission Privacy
- Image and Resource Privacy
- Content Security Policy (CSP)
- URL Parameter Privacy
- Cross-Origin Resource Sharing (CORS) and Privacy
- Best Practices Summary
- Implementation Checklist
- Industry-Specific Considerations
- Testing Your Implementation
- Common Mistakes
- Conclusion
- Related Documentation
Why HTML-Level Privacy Matters
The Referrer Problem
When users click links on your website, browsers send referrer headers that reveal:
- Source website: Where the user came from
- Page URL: The specific page they were on
- Context: Information about their browsing behavior
Example Referrer Leakage:
User visits: https://healthcare.com/patient-portal/diabetes-treatment
Clicks link to: https://external-site.com
Referrer sent: https://healthcare.com/patient-portal/diabetes-treatment
Privacy Impact:
- Third parties learn users visited healthcare pages
- Sensitive page URLs exposed (medical conditions, financial info)
- Cross-site tracking enabled through referrer data
- Compliance violations (HIPAA, GLBA, privacy regulations)
Beyond Consent Management
Consent management platforms control what scripts run, but don't prevent:
- Referrer header leakage
- Link tracking through URL parameters
- Cross-site data exposure
- Browser fingerprinting through technical attributes
Privacy engineering practices address these gaps at the HTML and technical level.
Referrer Policy Implementation
Understanding Referrer Policies
Referrer policies control what referrer information is sent with requests:
| Policy | Behavior | Use Case |
|---|---|---|
no-referrer | No referrer sent | Maximum privacy |
no-referrer-when-downgrade | No referrer on HTTPS→HTTP | Default browser behavior |
origin | Only origin (domain) sent | Balance privacy/functionality |
origin-when-cross-origin | Full URL same-origin, origin only cross-origin | Recommended default |
same-origin | Referrer only for same-origin requests | Internal sites |
strict-origin | Origin only, never on downgrade | Good balance |
strict-origin-when-cross-origin | Full URL same-origin, origin cross-origin, never on downgrade | Modern default |
unsafe-url | Always send full URL | Not recommended |
Meta Tag Referrer Policy
Set a site-wide referrer policy using a meta tag:
<!-- Set site-wide referrer policy -->
<meta name="referrer" content="strict-origin-when-cross-origin">
Recommended Settings:
For Healthcare/Financial Sites (Maximum Privacy):
<meta name="referrer" content="no-referrer">
For General Sites (Balanced):
<meta name="referrer" content="strict-origin-when-cross-origin">
For Internal Sites:
<meta name="referrer" content="same-origin">
HTTP Header Referrer Policy
Set referrer policy via HTTP headers (takes precedence over meta tags):
Nginx Configuration:
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Apache Configuration:
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Express.js (Node.js):
app.use((req, res, next) => {
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
Per-Link Referrer Policy
Override site-wide policy for specific links:
<!-- No referrer for this specific link -->
<a href="https://external-site.com"
rel="noreferrer">
External Link
</a>
<!-- Origin-only referrer for this link -->
<a href="https://external-site.com"
referrerpolicy="origin">
External Link
</a>
Anchor Link Privacy Attributes
rel="noreferrer"
Prevents referrer from being sent with the request:
<!-- No referrer sent -->
<a href="https://external-site.com" rel="noreferrer">
External Link
</a>
When to Use:
- Links to external third-party sites
- Links from sensitive pages (healthcare, financial)
- Links to social media platforms
- Any link where referrer leakage is a privacy concern
rel="noopener"
Prevents the new page from accessing the window.opener property:
<!-- Prevents opener access -->
<a href="https://external-site.com"
target="_blank"
rel="noopener">
External Link
</a>
Security Benefit:
- Prevents new page from accessing
window.opener - Prevents potential security vulnerabilities
- Should always be used with
target="_blank"
Combined: rel="noopener noreferrer"
Use both attributes together for maximum privacy and security:
<!-- Maximum privacy and security -->
<a href="https://external-site.com"
target="_blank"
rel="noopener noreferrer">
External Link
</a>
Best Practice:
Always use rel="noopener noreferrer" for external links that open in new tabs.
Automatic Application
For All External Links:
// Automatically add rel="noopener noreferrer" to external links
document.querySelectorAll('a[href^="http"]').forEach(link => {
const href = link.getAttribute('href');
const currentDomain = window.location.hostname;
const linkDomain = new URL(href).hostname;
if (linkDomain !== currentDomain) {
// External link - add privacy attributes
const rel = link.getAttribute('rel') || '';
if (!rel.includes('noopener')) {
link.setAttribute('rel', (rel + ' noopener noreferrer').trim());
}
}
});
WordPress Function:
// Automatically add privacy attributes to external links
function add_privacy_to_external_links($content) {
$content = preg_replace_callback(
'/<a\s+([^>]*href=["\']([^"\']*)["\'][^>]*)>/i',
function($matches) {
$href = $matches[2];
$attrs = $matches[1];
// Check if external link
if (strpos($href, 'http') === 0 &&
strpos($href, home_url()) !== 0) {
// Add rel attributes if not present
if (strpos($attrs, 'rel=') === false) {
$attrs .= ' rel="noopener noreferrer"';
} else {
$attrs = preg_replace(
'/rel=["\']([^"\']*)["\']/i',
'rel="$1 noopener noreferrer"',
$attrs
);
}
}
return '<a ' . $attrs . '>';
},
$content
);
return $content;
}
add_filter('the_content', 'add_privacy_to_external_links');
Form Submission Privacy
Preventing Referrer Leakage on Forms
Forms can leak referrer information when submitted to third parties:
<!-- DANGEROUS: Form submits to third party with referrer -->
<form action="https://third-party-service.com/submit" method="post">
<input type="text" name="data">
<button type="submit">Submit</button>
</form>
Privacy-Safe Form Submission:
<!-- SAFE: Form with referrer policy -->
<form action="https://third-party-service.com/submit"
method="post"
referrerpolicy="no-referrer">
<input type="text" name="data">
<button type="submit">Submit</button>
</form>
Server-Side Form Handling
For maximum privacy, handle form submissions server-side:
<!-- Form submits to your server first -->
<form action="/api/submit-form" method="post">
<input type="text" name="data">
<button type="submit">Submit</button>
</form>
// Server-side forwarding without referrer
app.post('/api/submit-form', (req, res) => {
// Forward to third party without referrer
fetch('https://third-party-service.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// No Refer header sent
},
referrerPolicy: 'no-referrer',
body: JSON.stringify(req.body)
});
});
Image and Resource Privacy
Image Referrer Policy
Images loaded from external domains can leak referrer information:
<!-- DANGEROUS: Image loads with referrer -->
<img src="https://external-cdn.com/image.jpg" alt="Image">
Privacy-Safe Image Loading:
<!-- SAFE: Image with referrer policy -->
<img src="https://external-cdn.com/image.jpg"
alt="Image"
referrerpolicy="no-referrer">
CSS and JavaScript Resources
External CSS and JavaScript files can also leak referrer:
<!-- DANGEROUS: External script with referrer -->
<script src="https://external-cdn.com/script.js"></script>
Privacy-Safe Resource Loading:
<!-- SAFE: Script with referrer policy -->
<script src="https://external-cdn.com/script.js"
referrerpolicy="no-referrer"></script>
Or use CSP (Content Security Policy):
<meta http-equiv="Content-Security-Policy"
content="referrer no-referrer">
Subresource Integrity (SRI)
The Domain Hijacking Risk
When you load external JavaScript or CSS files directly from third-party domains, you risk:
Domain Hijacking Scenario:
- You load a polyfill from
cdn.example.com/polyfill.js - Domain expires or is acquired by malicious actor
- Malicious actor stands up the same script name
- Injects malicious code alongside or instead of legitimate code
- Your site loads malicious script - security breach occurs
Real-World Example:
- Popular JavaScript library hosted on external CDN
- Domain registration expires
- Malicious party acquires domain
- Serves malicious version of the script
- All sites loading from that domain are compromised
What is Subresource Integrity (SRI)?
Subresource Integrity (SRI) is a security feature that allows browsers to verify that resources they fetch haven't been tampered with. It uses cryptographic hashes to ensure the integrity of external resources.
How SRI Works:
- Generate hash of the legitimate resource file
- Include hash in the
integrityattribute - Browser verifies downloaded resource matches hash
- Resource blocked if hash doesn't match
Implementation
Basic SRI Implementation:
<!-- DANGEROUS: External script without integrity check -->
<script src="https://cdn.example.com/polyfill.js"></script>
<!-- SAFE: External script with Subresource Integrity -->
<script src="https://cdn.example.com/polyfill.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
Why crossorigin="anonymous" is Required:
- SRI requires CORS to work properly
crossorigin="anonymous"enables CORS for integrity checking- Without it, SRI validation may fail
Generating SRI Hashes
Using OpenSSL:
# Generate SHA-384 hash (recommended)
cat polyfill.js | openssl dgst -sha384 -binary | openssl base64 -A
# Output: sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
Using Online Tools:
Using Node.js:
const crypto = require('crypto');
const fs = require('fs');
const fileBuffer = fs.readFileSync('polyfill.js');
const hash = crypto.createHash('sha384').update(fileBuffer).digest('base64');
console.log(`sha384-${hash}`);
CSS Resources with SRI
SRI also works for CSS files:
<!-- DANGEROUS: External CSS without integrity -->
<link rel="stylesheet" href="https://cdn.example.com/styles.css">
<!-- SAFE: External CSS with Subresource Integrity -->
<link rel="stylesheet"
href="https://cdn.example.com/styles.css"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
Multiple Hash Algorithms
You can provide multiple hash algorithms for compatibility:
<script src="https://cdn.example.com/polyfill.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
sha512-abc123def456..."
crossorigin="anonymous"></script>
Hash Algorithm Priority:
- Browser uses the strongest algorithm it supports
- Falls back to weaker algorithms if need
- SHA-384 is recommended (balance of security and compatibility)
Common CDN Libraries with SRI
Bootstrap:
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
jQuery:
<script src="https://code.jquery.com/jquery-3.7.0.min.js"
integrity="sha384-NXgwF8Kv9SSAr+7KKbfCOf+QqXuNSLQqIvokz8qU7lPfiU/TWuCMEmZNvXGkfNg"
crossorigin="anonymous"></script>
React:
<script src="https://unpkg.com/react@18/umd/react.production.min.js"
integrity="sha384-..."
crossorigin="anonymous"></script>
Updating SRI Hashes
When to Update:
- When updating library versions
- When CDN changes file content
- When security patches are applied
Update Process:
- Download new version of the resource
- Generate new SRI hash
- Update
integrityattribute - Test that resource loads correctly
Browser Support
SRI Browser Support:
- ✅ Chrome 45+
- ✅ Firefox 43+
- ✅ Safari 11+
- ✅ Edge 17+
- ✅ Opera 32+
Fallback Behavior:
- Browsers without SRI support ignore the
integrityattribute - Resource loads normally (no integrity check)
- Consider this when deciding whether to use SRI
Best Practices
1. Always Use SRI for External Scripts
<!-- Always include integrity for external scripts -->
<script src="https://cdn.example.com/script.js"
integrity="sha384-..."
crossorigin="anonymous"></script>
2. Use SRI for External CSS
<!-- Include integrity for external stylesheets -->
<link rel="stylesheet"
href="https://cdn.example.com/styles.css"
integrity="sha384-..."
crossorigin="anonymous">
3. Prefer Self-Hosted Resources
When Possible:
- Download and host resources on your own domain
- Eliminates domain hijacking risk entirely
- Full control over resource integrity
When External is Necessary:
- Use SRI to verify integrity
- Monitor for domain changes
- Have fallback self-hosted versions ready
4. Monitor for Hash Mismatches
// Monitor for SRI failures
window.addEventListener('error', function(e) {
if (e.target.tagName === 'SCRIPT' || e.target.tagName === 'LINK') {
if (e.target.hasAttribute('integrity')) {
console.error('SRI integrity check failed for:', e.target.src || e.target.href);
// Alert security team
}
}
}, true);
Common Mistakes
❌ Mistake: Missing crossorigin Attribute
Problem: SRI requires crossorigin="anonymous" to work properly.
Solution: Always include crossorigin="anonymous" with SRI.
<!-- WRONG: Missing crossorigin -->
<script src="https://cdn.example.com/script.js"
integrity="sha384-..."></script>
<!-- CORRECT: Includes crossorigin -->
<script src="https://cdn.example.com/script.js"
integrity="sha384-..."
crossorigin="anonymous"></script>
❌ Mistake: Outdated Hash Values
Problem: Hash doesn't match updated resource, causing resource to be blocked.
Solution: Update hash values when resources are updated.
❌ Mistake: Trusting CDN Without Verification
Problem: Assuming CDN-provided hashes are correct without verification.
Solution: Generate your own hashes from downloaded files.
Content Security Policy (CSP)
Referrer Policy in CSP
Set referrer policy via Content Security Policy:
<meta http-equiv="Content-Security-Policy"
content="referrer no-referrer">
CSP Header (Recommended):
add_header Content-Security-Policy "referrer no-referrer" always;
Comprehensive CSP
Combine referrer policy with other CSP directives:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
referrer no-referrer">
URL Parameter Privacy
Removing Sensitive URL Parameters
URL parameters can leak sensitive information:
<!-- DANGEROUS: Sensitive data in URL -->
<a href="https://external-site.com?source=healthcare&page=diabetes-treatment">
External Link
</a>
Privacy-Safe URLs:
<!-- SAFE: No sensitive parameters -->
<a href="https://external-site.com" rel="noreferrer">
External Link
</a>
Server-Side URL Sanitization
Sanitize URLs before redirecting or linking:
// Remove sensitive parameters before redirect
function sanitizeUrl(url) {
const urlObj = new URL(url);
// Remove sensitive parameters
const sensitiveParams = ['source', 'ref', 'utm_source', 'page', 'id'];
sensitiveParams.forEach(param => {
urlObj.searchParams.delete(param);
});
return urlObj.toString();
}
Cross-Origin Resource Sharing (CORS) and Privacy
CORS Headers and Referrer
CORS policies can affect referrer handling:
// Server-side CORS configuration
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Referrer-Policy', 'no-referrer');
next();
});
Best Practices Summary
1. Site-Wide Referrer Policy
Set a default referrer policy for your entire site:
<!-- In <head> section -->
<meta name="referrer" content="strict-origin-when-cross-origin">
Or via HTTP header:
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
2. External Link Privacy
Always use privacy attributes on external links:
<a href="https://external-site.com"
target="_blank"
rel="noopener noreferrer">
External Link
</a>
3. Form Submission Privacy
Prevent referrer leakage on forms:
<form action="https://external-service.com/submit"
referrerpolicy="no-referrer">
<!-- Form fields -->
</form>
4. Image and Resource Privacy
Set referrer policy on external resources:
<img src="https://external-cdn.com/image.jpg"
referrerpolicy="no-referrer">
5. Subresource Integrity (SRI)
Always use SRI for external JavaScript and CSS:
<script src="https://cdn.example.com/script.js"
integrity="sha384-..."
crossorigin="anonymous"></script>
<link rel="stylesheet"
href="https://cdn.example.com/styles.css"
integrity="sha384-..."
crossorigin="anonymous">
6. Sensitive Page Protection
For healthcare, financial, or sensitive pages:
<!-- Maximum privacy for sensitive pages -->
<meta name="referrer" content="no-referrer">
Implementation Checklist
HTML-Level Privacy
- Set site-wide referrer policy meta tag
- Add
rel="noopener noreferrer"to all external links - Set referrer policy on forms submitting to third parties
- Set referrer policy on external images and resources
- Add Subresource Integrity (SRI) to all external scripts and CSS
- Include
crossorigin="anonymous"with SRI attributes - Remove sensitive parameters from URLs
Server Configuration
- Configure HTTP referrer policy header
- Set Content Security Policy with referrer policy
- Configure CORS headers appropriately
- Sanitize URLs before redirects
Code-Level Implementation
- Automatically add privacy attributes to external links
- Sanitize form data before forwarding to third parties
- Remove sensitive URL parameters
- Implement server-side form handling for third-party submissions
Industry-Specific Considerations
Healthcare Websites
Maximum Privacy Required:
<!-- No referrer for healthcare sites -->
<meta name="referrer" content="no-referrer">
Why: Prevents leakage of medical page URLs, patient portal access, and health condition information.
Financial Services Websites
Strict Privacy Required:
<!-- Strict referrer policy for financial sites -->
<meta name="referrer" content="no-referrer">
Why: Prevents leakage of account information, transaction pages, and financial data.
General Websites
Balanced Approach:
<!-- Balanced privacy for general sites -->
<meta name="referrer" content="strict-origin-when-cross-origin">
Why: Maintains functionality while protecting privacy.
Testing Your Implementation
Verify Referrer Policy
Browser Console Test:
// Check current referrer policy
console.log('Referrer Policy:', document.referrerPolicy);
// Test link referrer
const link = document.createElement('a');
link.href = 'https://example.com';
link.rel = 'noreferrer';
link.click();
// Check Network tab - no Refer header should be sent
Network Tab Verification
- Open Browser DevTools → Network tab
- Click external link with
rel="noreferrer" - Check Request Headers → Should not include
Referheader - Verify Privacy → No source URL information leaked
Automated Testing
// Test that external links have privacy attributes
function testExternalLinkPrivacy() {
const externalLinks = document.querySelectorAll('a[href^="http"]');
const currentDomain = window.location.hostname;
externalLinks.forEach(link => {
const linkDomain = new URL(link.href).hostname;
if (linkDomain !== currentDomain) {
const rel = link.getAttribute('rel') || '';
if (!rel.includes('noreferrer')) {
console.warn('External link missing noreferrer:', link.href);
}
if (!rel.includes('noopener') && link.target === '_blank') {
console.warn('External link missing noopener:', link.href);
}
}
});
}
Common Mistakes
❌ Mistake: Only Using rel="noopener"
Problem: noopener prevents opener access but doesn't prevent referrer leakage.
Solution: Always use rel="noopener noreferrer" together.
❌ Mistake: Inconsistent Referrer Policies
Problem: Different pages have different referrer policies, creating confusion.
Solution: Set site-wide policy and override only when necessary.
❌ Mistake: Forgetting Form Referrer Policy
Problem: Forms submit to third parties with referrer information.
Solution: Set referrerpolicy="no-referrer" on forms submitting externally.
❌ Mistake: Sensitive URLs in Links
Problem: URL parameters contain sensitive information.
Solution: Remove sensitive parameters or use server-side URL sanitization.
❌ Mistake: Loading External Scripts Without SRI
Problem: External scripts loaded without integrity checks can be compromised if domain is hijacked.
Solution: Always include integrity and crossorigin="anonymous" attributes on external scripts and CSS.
Conclusion
Privacy engineering practices at the HTML and technical level complement consent management by preventing data leakage through referrer headers, link tracking, and other technical mechanisms. These practices are essential for protecting user privacy, especially for healthcare and financial services websites.
Key Takeaways:
- Set Referrer Policy: Use meta tags or HTTP headers to control referrer behavior
- External Link Privacy: Always use
rel="noopener noreferrer"on external links - Form Privacy: Prevent referrer leakage on forms submitting to third parties
- Resource Privacy: Set referrer policy on external images and scripts
- Subresource Integrity: Always use SRI for external JavaScript and CSS to prevent domain hijacking
- Sensitive Pages: Use
no-referrerpolicy for healthcare and financial pages
Rember: These practices work alongside consent management platforms—they don't replace consent management, but they add an essential layer of privacy protection at the technical level.
Related Documentation
- Consent Management Platform Best Practices - CMP implementation
- Social Media Integration Privacy Compliance - Privacy-safe social media
- Common Privacy Pitfalls - What to avoid
- Privacy Champion Guide - Building a privacy program
Note: These practices are technical privacy controls that complement consent management. They don't replace the need for proper consent management platforms, but they add essential privacy protection at the HTML and technical level.