Technical article

Back to the blog

Web security headers: a practical guide

A practical guide to HSTS, CSP, Permissions-Policy and other security headers: what they mitigate, how to apply them and how to test them.

Published
Reading time
9 minutes
Topics
Seguridad · Frontend · Web

Security headers are one of those parts of web development that usually appear late. First you learn HTML, CSS, JavaScript, routes, components, forms, SEO, performance. Then one day you run an audit tool or read about web security and realise your server can send instructions to the browser to reduce some important risks.

They are not a magic fix. A header will not repair a vulnerable application, validate forms for you or replace dependency checks, permissions or server-side logic.

But they are part of a professional website.

The useful part is understanding what problem each header tries to reduce. Copying a configuration from the internet without knowing what it does can break images, scripts, analytics, iframes or external integrations. In security, a configuration you do not understand is debt.

What an HTTP header is

When the browser requests a page, the server does not respond only with HTML. It also sends metadata as HTTP headers.

Some headers describe the content type. Others control caching. Others give security instructions.

For example, a response can tell the browser:

  • This page should only be loaded over HTTPS.
  • Do not allow other sites to embed it inside an iframe.
  • Do not execute scripts that are not allowed.
  • Do not share too much information in the Referer.
  • Do not allow access to camera, microphone or geolocation.

The header does not change the page content. It changes the rules the browser applies to that page.

Why frontend developers should care

For a long time, I thought of security as something closer to backend or systems work. Databases, passwords, servers, permissions.

But many security headers directly affect the frontend.

If you load third-party scripts, use Google Analytics, serve images, work with local fonts, have forms, publish a PWA or want to stop your website being embedded somewhere else, headers affect you.

Frontend developers also make decisions that shape security later:

  • Adding third-party scripts.
  • Using iframes.
  • Loading external fonts.
  • Allowing images from other domains.
  • Injecting dynamic HTML.
  • Connecting to APIs.
  • Using analytics or tracking pixels.

That is why it is worth knowing these headers even if you are not a cybersecurity specialist. Not to memorise every directive, but to know which questions to ask before publishing.

Strict-Transport-Security

Strict-Transport-Security, usually shortened to HSTS, tells the browser that a site must always be loaded over HTTPS for a specific amount of time.

Example:

Strict-Transport-Security: max-age=31536000; includeSubDomains

The goal is to reduce the risk of someone forcing an insecure HTTP connection.

If a person visits your site once over HTTPS and receives this header, the browser remembers that it should use HTTPS on future visits. Even if the user types http://, the browser will try HTTPS.

It is a useful header, but you should not blindly enable it for subdomains if you do not control the whole infrastructure. includeSubDomains means it also affects subdomains. If one of them is not ready for HTTPS, you can break access.

For a professional website on its own domain, HTTPS should be mandatory. HSTS helps reinforce that.

Content-Security-Policy

Content-Security-Policy, or CSP, is one of the most powerful headers and also one of the easiest to get wrong.

It tells the browser which sources are allowed for scripts, styles, images, fonts, connections, iframes and other resources.

A simplified example:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'

The idea is to reduce the impact of attacks such as XSS. If an attacker manages to inject a script, a good CSP can stop that script from running or loading resources from an untrusted domain.

But this is where you need to be careful. A CSP that is too strict can break the site. A CSP that is too loose barely helps.

For example, if your site uses Google Analytics, you will need to allow certain domains in script-src and connect-src. If you serve external images, that needs to be reflected in img-src. If you use inline styles, you need to decide whether to allow them, use hashes or change the way styles are loaded.

Common directives include:

  • default-src: fallback rule when there is no more specific directive.
  • script-src: where scripts can be loaded from.
  • style-src: where styles can be loaded from.
  • img-src: where images can be loaded from.
  • font-src: where fonts can be loaded from.
  • connect-src: which domains the site can connect to through fetch, XHR, WebSocket or analytics.
  • object-src: controls resources such as <object> or <embed>. In many modern sites it can be 'none'.
  • base-uri: limits use of the <base> element.
  • frame-ancestors: controls who can embed your page in an iframe.

A good way to work with CSP is to start with Content-Security-Policy-Report-Only, review what would break and then apply the real policy.

On a small static site, you can usually get to a fairly strict CSP. On an app with many external integrations, you will need more patience.

X-Content-Type-Options

X-Content-Type-Options is a simple header:

X-Content-Type-Options: nosniff

It tells the browser not to guess the file type if the server has already sent a Content-Type.

This matters because some browsers historically tried to interpret resources flexibly. If a file was served with the wrong type, they could treat it as JavaScript or CSS even when it was not.

With nosniff, you reduce that behaviour.

It is not a complicated header and it usually makes sense on almost any website. The condition is serving correct content types. If you send CSS as text/plain, the browser may block it, and it would be right to do so.

Referrer-Policy

When someone clicks from your website to another page, the browser may send information about the source URL through the Referer header.

Yes, it is written Referer, with the historical typo.

Referrer-Policy lets you control how much information you share.

A reasonable option for many sites is:

Referrer-Policy: strict-origin-when-cross-origin

With this policy, full URLs can be sent when navigating within the same site. When leaving to another origin, only the origin is sent, not the full path. And if the destination is less secure, for example from HTTPS to HTTP, nothing is sent.

This helps avoid leaking internal URLs with parameters or sensitive paths to other domains.

Not every site needs the same policy. If your analytics or integrations depend on specific referrer data, you need to check that. But sending the full URL to every destination is usually more exposure than necessary.

Permissions-Policy

Permissions-Policy limits which browser capabilities your page or its iframes can use.

For example:

Permissions-Policy: camera=(), microphone=(), geolocation=()

This says the page cannot use camera, microphone or geolocation.

For a content site, portfolio or blog, you usually do not need access to camera, microphone, geolocation, payments, fullscreen or screen capture. Blocking what you do not use reduces surface area and makes the intent of the site clearer.

It is not that a page can turn on the camera without user permission. The browser already asks for permission. But if your site does not need that capability, there is no reason to leave it available.

This header is especially useful for applying the principle of least privilege: allow only what is needed.

X-Frame-Options and frame-ancestors

Clickjacking means tricking a user into interacting with a legitimate page embedded inside another page.

A classic defence was:

X-Frame-Options: DENY

Or:

X-Frame-Options: SAMEORIGIN

Today, if you use CSP, the more flexible and modern option is frame-ancestors.

For example:

Content-Security-Policy: frame-ancestors 'none'

That means nobody can embed your page in an iframe.

For a portfolio, blog or simple business website, it usually makes sense to block other sites from embedding you. If your project needs to be embedded on other domains, then you should allow those specific origins.

The practical question is simple: is there a real reason for another website to load this page inside an iframe?

If the answer is no, block it.

Cross-Origin-Opener-Policy

Cross-Origin-Opener-Policy, or COOP, controls how your page relates to other windows or tabs from a different origin.

A common configuration is:

Cross-Origin-Opener-Policy: same-origin

This helps isolate your document from other origins. It can reduce certain risks related to opened windows, cross-context access and browser isolation.

But it can also break flows that depend on external popups, such as some login or payment integrations. That is why a more compatible option is sometimes used:

Cross-Origin-Opener-Policy: same-origin-allow-popups

This is not usually the first header I would configure when starting out. I would understand CSP, HSTS, nosniff, Referrer-Policy and Permissions-Policy first. But it is worth knowing because it appears more often in modern audits and configurations.

Cross-Origin-Resource-Policy

Cross-Origin-Resource-Policy, or CORP, lets you indicate who can load a resource from another origin.

For example:

Cross-Origin-Resource-Policy: same-site

Or:

Cross-Origin-Resource-Policy: cross-origin

It is not used the same way for every resource. For internal assets, it may make sense to restrict more. For public images, favicons or resources you want to allow externally, you may need a more open policy.

On my site, for example, it makes sense to think differently about generated assets, public images and HTML pages. Not everything needs the same rule.

This is an important idea: headers do not have to be identical across the whole site. A sitemap, a text file, an image and an HTML page do not behave the same way.

Cache-Control also affects security

Cache-Control is not usually listed as a security header, but it can have security implications.

On a static website, caching hashed assets for a long time is reasonable:

Cache-Control: public, max-age=31536000, immutable

If the filename changes when the content changes, the browser can safely keep it for a long time.

But you would not apply the same logic to a page with sensitive information, a personalised response or a document you do not want stored in shared caches.

On a site like a portfolio, the risk is lower because the content is public. Still, it is worth understanding that caching is not only performance. It also defines where and for how long a response can be stored.

Headers you should not copy by habit

Some old headers or configurations still appear in articles, but they do not always make sense today.

A typical example is:

X-XSS-Protection: 1; mode=block

For years, it was used to enable browser XSS filters. Today it is obsolete in modern browsers and can even create unwanted behaviour. It is better to invest effort in a well-designed CSP and in preventing injection at the source.

You should also be careful with copying huge CSP policies without understanding them. If you see a configuration full of domains, hashes and exceptions, it probably belongs to a specific website. That does not mean it is good for yours.

The practical rule is simple: if you do not know what resource a directive allows, do not paste it yet.

How I would apply this on a small website

For a simple professional website, such as a portfolio, blog or landing page without a private area, I would start with these:

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

I would not copy this exactly without testing.

CSP will almost always need adjustments. If you use analytics, external fonts, remote images or third-party scripts, you will need to allow them explicitly.

A reasonable order would be:

  1. Enable HTTPS correctly.
  2. Add HSTS once the domain is ready.
  3. Add nosniff and Referrer-Policy.
  4. Define a restrictive Permissions-Policy.
  5. Design a minimal CSP.
  6. Test locally, in staging or in report-only mode.
  7. Check the browser console and real responses.
  8. Adjust only what is necessary.

This does not need to become an obsession. It needs to become a normal part of publishing a website.

How to check your headers

There are several simple ways:

  • Open DevTools, go to Network, select a request and inspect Response Headers.
  • Use curl -I https://your-domain.com.
  • Run tools such as Security Headers.
  • Review your hosting configuration.
  • Check not only the home page, but also internal pages, images and technical files.

The important part is checking the real production response. It is not enough for the configuration file to look correct.

You also need to review after adding external scripts. A CSP often works until you add analytics, a widget, an embedded form or a third-party resource.

What I take from this as a developer

Security headers are a good example of something a web developer should understand even without being a security specialist.

You do not need to memorise every directive. You need to understand the risks and the basic controls you can apply.

For me, the main idea is this:

A professional website should not only look good and load fast. It should also tell the browser what is allowed and what is not.

That reduces room for error, forces you to think about the resources you load and makes the website more explicit.

If you are starting out, do not try to configure every possible header on the same day. Start by understanding HSTS, CSP, nosniff, Referrer-Policy and Permissions-Policy. That already gives you a much more serious foundation than just publishing the site and hoping everything is fine.

Security does not appear suddenly at the end of a project. It is built through small correct decisions from the beginning.