How to Add HSTS and Referrer Policy Headers
Add HSTS and Referrer-Policy headers so the browser always uses HTTPS and does not leak the URL to cross-origin destinations.
On this page
Introduction
The hardest part of set browser security is not writing the code — it is deciding what the code is allowed to reject. A security headers setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in security headers, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
Choosing security headers for set browser security is a trade, not a winner. You give up some flexibility to get a hard boundary, and the exchange pays off when the boundary catches a class of bugs that would otherwise surface at 3 a.m. Document the trade at the call site so the next reader does not undo the guard thinking it is a bug. The reader who does not know why the guard exists is the reader who removes it.
The sections below walk through the concrete decisions: where to put the security headers check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Set HSTS
Set HSTS is the first decision because it determines what the rest of the security headers pipeline can assume. A wrong choice here turns into set browser security under load, and the invariant you thought security headers guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Set HSTS is the one most teams skip: drive security headers to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
The javascript above is the shape to ship for set browser security. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Set Referrer-Policy
The mechanism behind Set Referrer-Policy is straightforward in security headers, but the consequences ripple. A wrong choice here turns into set browser security under load, and the invariant you thought security headers guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Set Referrer-Policy is the one most teams skip: drive security headers to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Set X-Content-Type-Options
Set X-Content-Type-Options is where most security headers implementations leak performance. A wrong choice here turns into set browser security under load, and the invariant you thought security headers guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Set X-Content-Type-Options is the one most teams skip: drive security headers to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
test('sets HSTS', () => {
expect(res.headers['strict-transport-security']).toContain('max-age=31536000');
});
Pin the behavior with this test. A security headers setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Test with a scanner
The hard part of Test with a scanner is not the happy path — it is what happens when security headers fails halfway. A wrong choice here turns into set browser security under load, and the invariant you thought security headers guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test with a scanner is the one most teams skip: drive security headers to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Include preload
Include preload should be observable without a debugger. A wrong choice here turns into set browser security under load, and the invariant you thought security headers guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Include preload is the one most teams skip: drive security headers to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test headers
Test headers is a trade, not a winner. A wrong choice here turns into set browser security under load, and the invariant you thought security headers guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test headers is the one most teams skip: drive security headers to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in set browser security all reduce to one mistake: treating security headers as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the security headers boundary so set browser security enters unvalidated.
- Assuming security headers is fast without measuring the tail latency under load.
- Letting set browser security fail silently instead of failing loudly with a metric.
- Testing only the happy path so the security headers failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: security headers fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives security headers to the failing condition and assert the system degrades gracefully. Then set browser security is a property you maintain, not a feature you hope for.
Sources: HSTS, Referrer-Policy
FAQ
- HSTS preload?
- Submit to the preload list. Then browsers enforce HTTPS even on first visit.
- Referrer-Policy?
- strict-origin-when-cross-origin sends the origin only to cross-origin, full to same-origin.
Report a problem with this articleOpens a pre-filled issue on GitHub.