There is a free test you can run right now on your business website. Go to securityheaders.com, enter your domain, and click scan. You will get a grade from A+ to F. If your website was built by a typical web agency in Ticino, you are going to see an F. Maybe a D if you are lucky.
That F means your website is missing HTTP security headers that every site should have. These headers are instructions from your web server to the visitor's browser, telling it how to behave when loading your site. Without them, the browser operates with default settings that leave your site and your visitors vulnerable to several categories of attacks.
This article explains each header in plain language, shows you what happens when they are missing, and provides implementation instructions for the most common server configurations.
What HTTP Security Headers Are
When your browser requests a web page, the server responds with the page content and a set of HTTP headers. Headers carry metadata: the content type, caching instructions, cookie settings, and (when configured) security policies. Security headers are a subset of these that instruct the browser to enable or restrict specific behaviors.
They cost nothing to implement. They require no software purchase. They are configuration changes on your web server or CDN. And they block entire categories of attacks that would otherwise be possible. The fact that most websites do not have them is a failure of the agencies and developers who built those sites.
Content-Security-Policy (CSP)
CSP is the most powerful and most complex security header. It tells the browser exactly which sources of content are allowed on your page: which domains can serve JavaScript, which domains can serve CSS, which domains can serve images, whether inline scripts are allowed, and so on.
What It Prevents
Cross-Site Scripting (XSS). If an attacker manages to inject a <script> tag into your page (through a form vulnerability, a compromised third-party library, or a stored XSS attack), CSP prevents the browser from executing it because the script does not match the policy. The injected code is dead on arrival.
Without CSP, the browser has no way to distinguish between your legitimate scripts and an attacker's injected script. It executes everything.
Example Policy
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'none';
This policy says: by default, only load content from our own domain ('self'). JavaScript only from our domain and Google Tag Manager. Styles from our domain (and allow inline styles, which many sites need). Images from our domain, data URIs, and any HTTPS source. Fonts from our domain and Google Fonts. AJAX requests only to our domain and Google Analytics. No other site is allowed to embed us in a frame.
Why Most Agencies Do Not Implement It
CSP requires understanding exactly what your site loads and from where. A typical business website might load scripts from Google Analytics, Google Tag Manager, Facebook Pixel, LinkedIn Insight, a chat widget, a cookie consent tool, embedded YouTube videos, Google Maps, and three different font services. Each of these must be explicitly whitelisted in the CSP.
If you get the policy wrong, things break. Scripts stop loading. Widgets disappear. The agency does not want to deal with the debugging, so they skip CSP entirely. This is understandable but not acceptable. A site without CSP is significantly more vulnerable to XSS.
Starting Point: Report-Only Mode
You do not have to enforce CSP immediately. Start with Content-Security-Policy-Report-Only. This header does not block anything. It logs violations so you can see what would be blocked, adjust the policy, and then switch to enforcement when you are confident.
Strict-Transport-Security (HSTS)
HSTS tells the browser: "Always use HTTPS when connecting to this site. Never use HTTP. Even if the user types http:// or clicks an HTTP link."
What It Prevents
SSL stripping attacks. Without HSTS, an attacker on the same network (a public WiFi, for example) can intercept the initial HTTP request before it redirects to HTTPS and downgrade the connection to unencrypted HTTP. The user thinks they are on HTTPS (the address bar might not be clearly visible), but the attacker sees everything in plain text: login credentials, session cookies, personal data.
Example Header
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
This says: for the next year (31,536,000 seconds), always use HTTPS for this domain and all its subdomains. The preload directive allows your domain to be added to browser preload lists, which means even the very first connection will be HTTPS (before the browser has seen the HSTS header).
Important Prerequisite
Before enabling HSTS, make sure everything works correctly over HTTPS. All pages, all resources (images, scripts, stylesheets), all subdomains. HSTS cannot be easily undone once the browser has cached it. If you enable it and something does not work over HTTPS, visitors will be locked out until the max-age expires or they manually clear their browser's HSTS cache.
Learn more about SSL/TLS configuration in our article on SSL certificates and what they actually protect.
X-Frame-Options
X-Frame-Options tells the browser whether your site can be embedded inside an iframe on another website.
What It Prevents
Clickjacking. An attacker creates a page with your website loaded in a transparent iframe on top of a decoy page. The user thinks they are clicking buttons on the decoy page, but they are actually clicking on your hidden site. This can trick users into changing account settings, making purchases, transferring money, or performing any action they would normally do on your site.
Example Header
X-Frame-Options: DENY
This prevents your site from being embedded in any iframe, anywhere. If you need to allow embedding on your own domain (for admin panels or preview features), use SAMEORIGIN instead of DENY.
Note: CSP's frame-ancestors directive is the modern replacement for X-Frame-Options and provides more granular control. However, X-Frame-Options is still recommended for backward compatibility with older browsers.
X-Content-Type-Options
This header has a single valid value: nosniff.
X-Content-Type-Options: nosniff
What It Prevents
MIME type sniffing. Without this header, browsers will sometimes try to "guess" the content type of a file by examining its contents rather than trusting the Content-Type header sent by the server. This can be exploited: an attacker uploads a file that the server serves as text/plain, but the browser sniffs it as text/html and renders it, executing embedded JavaScript. With nosniff, the browser strictly follows the server's Content-Type declaration and does not attempt to interpret the content differently.
There is no reason not to set this header on every site. It has no side effects.
Referrer-Policy
When a user clicks a link from your site to another site, the browser sends a Referer header (yes, it is misspelled in the standard) to the destination site, telling it where the user came from. This can leak sensitive information: internal page names, search queries, logged-in URLs, or any other data present in the URL.
Example Header
Referrer-Policy: strict-origin-when-cross-origin
This policy sends the full referrer for same-origin requests (within your own site), sends only the origin (domain) for cross-origin requests over HTTPS, and sends nothing when navigating from HTTPS to HTTP. This is a good balance between functionality (analytics tools need some referrer data) and privacy.
Common Options
| Policy | Behavior | Use Case |
|---|---|---|
no-referrer | Never send referrer | Maximum privacy, breaks some analytics |
strict-origin-when-cross-origin | Full path for same-origin, origin only for cross-origin | Recommended default |
same-origin | Full referrer for same-origin, nothing for cross-origin | When you do not want external sites to know your URLs |
origin | Only send the domain, never the path | When analytics need the origin but not the path |
Permissions-Policy (formerly Feature-Policy)
Permissions-Policy controls which browser features your website can use: camera, microphone, geolocation, payment, autoplay, fullscreen, and many others. If your website does not need the camera, why should the browser allow it? Disabling unused features reduces the attack surface.
Example Header
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
This disables camera, microphone, geolocation, and payment API for your site and any embedded iframes. If an attacker injects malicious code that tries to access the camera, the browser blocks it because the policy says the camera is not allowed.
What Your Grade Means
When you test your site at securityheaders.com, each header adds to your score:
| Grade | Meaning |
|---|---|
| A+ | All recommended headers present and correctly configured. Your site follows security best practices. |
| A | Most headers present. Minor improvements possible. |
| B-C | Some headers present but key ones missing (usually CSP and/or Permissions-Policy). |
| D | Only basic headers present. Significant gaps in protection. |
| F | No or almost no security headers. The site relies entirely on browser defaults, which are permissive. |
An F does not mean your site has been hacked. It means your site is missing an entire layer of defense that costs nothing to implement. It means that if an XSS vulnerability exists somewhere in your code or a third-party script, there is no safety net to catch it.
Before and After: A Real Example
Here is what the response headers of a typical Swiss SME website look like before and after security header implementation:
Before (Grade: F)
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: Apache/2.4.51
X-Powered-By: PHP/7.4.27
Four headers. The server advertises its software version (useful for attackers), reveals the PHP version (useful for attackers), and provides zero security instructions to the browser. Also note: PHP 7.4 reached end of life in November 2022. This server is running unsupported software.
After (Grade: A)
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
The server version and PHP version are hidden. Six security headers instruct the browser to enforce strict content policies. The attack surface is dramatically reduced.
Why Most Web Agencies in Ticino Do Not Configure These
We audit dozens of websites per year in Ticino and across Switzerland. The pattern is consistent: security headers are missing in 80-90% of cases. The reasons are:
- Not part of the standard checklist. Most agencies have a development checklist that covers design, content, SEO basics, and maybe SSL. Security headers are not on it because nobody put them there.
- CSP is time-consuming to configure correctly. With the number of third-party scripts modern websites load, building a correct CSP takes testing and iteration. Agencies work on fixed budgets and timelines. Extra configuration time cuts into margins.
- Fear of breaking things. A misconfigured CSP can prevent scripts from loading, widgets from appearing, and analytics from tracking. Nobody wants a post-launch call from a client saying "my Google Analytics stopped working." So they skip it.
- Lack of security knowledge. Web development and web security are different disciplines. Many developers have never heard of Permissions-Policy or do not understand what CSP does. This is not a personal failure; it is a training gap in the industry.
- "The client did not ask for it." This is the most common justification. But clients do not ask for fire alarms either. Building standards require them. Security headers should be a baseline expectation, not an optional extra.
Implementation Guide
Apache (.htaccess or httpd.conf)
Add these lines to your .htaccess file or Apache virtual host configuration:
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always unset X-Powered-By
Header always unset Server
Make sure mod_headers is enabled. On most hosting environments it is enabled by default.
Nginx (server block)
Add these lines inside your server { } block:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
server_tokens off;
Cloudflare
If you use Cloudflare, you can set security headers through Transform Rules (HTTP Response Header Modification) in the Cloudflare dashboard. Navigate to Rules > Transform Rules > Modify Response Header, and add each header. Cloudflare also has a one-click HSTS setting under SSL/TLS > Edge Certificates.
Vercel / Netlify / Other Platforms
Most modern hosting platforms support security headers through configuration files (vercel.json, netlify.toml, _headers file). Check your platform's documentation for the specific syntax.
Testing After Implementation
After adding security headers:
- Run
securityheaders.comagain and verify your grade improved. - Test every page of your website. Especially pages with forms, embeds, videos, maps, and third-party widgets.
- Check the browser console (F12 > Console) for CSP violations. These show up as error messages telling you exactly what was blocked and why.
- Test on multiple browsers (Chrome, Firefox, Safari) since they handle headers slightly differently.
- Monitor Google Analytics and other tracking tools to make sure they still function correctly.
Common Pitfalls
- CSP too restrictive: Blocking
'unsafe-inline'for scripts breaks most WordPress sites (and many other CMS-based sites) because they use inline scripts extensively. You may need to use nonces or hashes for specific inline scripts. - HSTS on a site with mixed content: If some resources load over HTTP while the page is HTTPS, HSTS will not fix those resources. Fix mixed content first, then enable HSTS.
- Forgetting subdomains: If you set HSTS with
includeSubDomains, every subdomain must support HTTPS. An internal tool atinternal.yourcompany.chthat only works over HTTP will break. - Duplicate headers: If both your web server and your application set the same header, some browsers use the last one, others use the first. Avoid duplicates.
For a broader view of website security, read our detailed guide on implementing security headers step by step. And if you want us to implement security headers for your site and ensure nothing breaks, get in touch with our team.
Want to know if your site is secure?
Request a free security audit. In 48 hours you get a complete report.
Request Free Audit