🛡️ X-Frame-Options — Clickjacking Protection
🎯 What it does
Controls whether your site can be displayed inside an <iframe> on another website. Prevents external pages from embedding your content without permission.
⚠️ Why it matters
Without this protection, an attacker can load your site in a transparent iframe and trick users into clicking unknowingly (clickjacking). This can expose credentials, authorize payments, or change settings.
⚙️ Configuration options
- DENY (recommended): Blocks any attempt to display your site in an iframe, no exceptions.
- SAMEORIGIN: Only allows iframes from the same domain. Useful if you use legitimate iframes of your own.
<iframe>, positioned exactly over the "Participate" button. The victim — logged in as an administrator in another tab — thinks they're clicking "Participate", but the click actually lands on the real wp-admin button hidden underneath, such as "Delete user" or "Install plugin". Without X-Frame-Options, the browser allows your site to load inside that foreign iframe. With X-Frame-Options: DENY, the browser refuses outright to display your site inside any external iframe, leaving a blank gap on the attacker's page where the bait should be.
📝 X-Content-Type-Options — MIME Sniffing Prevention
🎯 What it does
Tells the browser NOT to guess the file type (MIME sniffing). Forces it to respect exactly the type declared in the Content-Type header.
⚠️ Why it matters
Without this header, a browser may interpret a text file as JavaScript and execute it. This allows attackers to upload a file with a .txt extension but with malicious code that the browser will execute.
⚙️ Configuration
The only valid value is nosniff. It is recommended to always enable it. It has no negative side effects.
image.jpg that actually contains JavaScript code disguised as an image. If a visitor's browser tries to "guess" the content type instead of trusting the header the server declares, it can end up interpreting that file as a script and running it in the context of your site, instead of showing it as a harmless picture. With X-Content-Type-Options: nosniff, the browser sticks strictly to the type the server declared (image), and refuses to execute it as code no matter what its internal content suggests.
🔒 X-XSS-Protection — Why We No Longer Send It
🎯 What was it?
A header that enabled a built-in anti-XSS filter in older browsers (Internet Explorer, old versions of Chrome and Safari), designed to detect and block reflected script injection attempts.
⚠️ Why we removed it
Chrome removed it entirely in 2019 — not that it just stopped having an effect, Google actually considered it more of a risk than a help, because the filter itself had documented security flaws and was fairly easy to bypass. Firefox never implemented it at all. Microsoft ended support for Internet Explorer in 2022. Keeping a header that no longer protects anything real, and that could once introduce its own problems, made no sense.
🔗 Referrer-Policy — Referrer Leakage Control
🎯 What it does
Controls how much information from the origin URL is sent in the Referer header when users click a link or load an external resource.
⚠️ Why it matters
Without control, the full URL (including parameters like tokens, session IDs, or personal data) can leak to external sites. This violates user privacy and can expose sensitive data.
⚙️ Configuration options
- no-referrer: Never sends the Referer. Maximum privacy, but may break analytics.
- same-origin: Only sends Referer within the same domain. Does not send to external sites.
- strict-origin: Sends only the origin (domain), never the full URL. Only on HTTPS-to-HTTPS connections.
- strict-origin-when-cross-origin (recommended): Sends full URL within the same domain, and only the origin to external sites. The best balance between functionality and privacy.
Referer header to the external server, without the user or the external site's owner explicitly asking for it. With Referrer-Policy: strict-origin-when-cross-origin, the browser only shares the origin domain with external sites, never the full path with the token, and links within your own domain keep working normally.
🔐 HSTS — Enforce HTTPS Connection
🎯 What it does
Forces browsers to connect exclusively via HTTPS, even if the user types http:// or clicks an HTTP link. The browser remembers this for the configured max-age.
⚠️ Why it matters
Without HSTS, an attacker on the same network (public Wi-Fi, for example) can intercept the first HTTP connection and redirect the user to a fake site (man-in-the-middle attack). HSTS eliminates this vulnerability window.
⚙️ Configuration: max-age
- max-age=0: Disables HSTS. Forces browsers to forget the policy.
- max-age=3600 (1 hour): For testing. Low risk, browsers forget quickly.
- max-age=63072000 (2 years, recommended): For production. Browsers will remember to enforce HTTPS for 2 years.
https:// prefix. The browser first attempts the insecure HTTP connection. An attacker on that same wifi network can intercept that first HTTP request and return a fake copy of your site to steal credentials, before the normal redirect to HTTPS completes (this is known as a man-in-the-middle attack). With HSTS active and the site already visited once before over HTTPS, the browser doesn't even attempt the insecure connection: it forces HTTPS directly, locally on the device itself, denying the attacker that interception window.
🎯 Content-Security-Policy (CSP) — The Content Police
🎯 What it does
Defines exactly which sources are authorized to load scripts, styles, images, fonts, and other resources on your site. Any unauthorized resource is automatically blocked by the browser.
⚠️ Why it matters
CSP is the most powerful defense against XSS. Even if an attacker manages to inject a malicious script, CSP blocks it because the script does not come from an authorized source. It is your last line of defense.
<script> tag that loads code from a foreign domain into one of your pages (a typical failure when a plugin doesn't correctly escape user input). Without CSP, any visitor's browser loading that page would execute the script without question, and the code could steal session cookies, redirect to a phishing site, or capture keystrokes. With a CSP that restricts script-src to your own domain (and any sources you've explicitly authorized), the browser detects that the foreign domain isn't on the allowed list and refuses to load and run the script, even though the malicious code is already sitting in the page's HTML.
⚙️ Configuration
CSP uses a free-text editor where you write the policy directly. There are no one-click template buttons.
default-src 'self';. That single directive acts as a fallback for EVERY resource type that has no explicit directive of its own (scripts, styles, images, fonts...), and since it does not include 'unsafe-inline' or any external domain, it will block the inline styles and scripts that almost any WordPress theme or plugin writes directly into the HTML, plus any resource loaded from an external CDN (Google Fonts, jQuery, YouTube, etc.). Do not turn the CSP switch on with this value unedited — it will very likely break the look and functionality of a real WordPress site.
unsafe-inline, a specific CDN domain, or a permission another site doesn't need. That's why the field is left blank (with the minimal fallback default-src 'self';) so you build it yourself based on what your site actually loads, instead of offering a button that invites enabling it blindly.
✍️ How a CSP Policy Is Written (Basic Syntax)
Even though it looks like an intimidating block of text, CSP always follows one very simple, consistent structure:
- Each directive ends with a semicolon (
;). That's what separates one rule from the next — if you forget one, the next directive can end up "glued" to the previous one and stop working. - Within a directive, values are separated by spaces (never commas):
script-src 'self' https://example.com https://other.com;authorizes three different sources for scripts. - Special keywords go in single quotes:
'self'(your own domain),'none'(nothing, completely forbidden),'unsafe-inline'(allows code written directly in the HTML). Regular domains never get quotes:https://fonts.googleapis.com, not'https://fonts.googleapis.com'. - You can authorize an entire domain with a wildcard:
https://*.googleapis.comallows any subdomain of googleapis.com, instead of having to list each one.
script-src for a script) within the text already in the field, and add the new domain right before that directive's semicolon, separated by a space from the previous value. Example: if you had script-src 'self' 'unsafe-inline'; and need to authorize https://widget.example.com, change it to: script-src 'self' 'unsafe-inline' https://widget.example.com;. Don't delete anything that was already there, just add.
📚 The Directives You'll Actually Use (How Many There Are and What Each Is For)
The CSP specification defines more than 20 directives in total, but on a normal WordPress site you'll only ever need to touch these 12 — they're the ones already in the default value or the ones most often needed when adding a new service:
| Directive | What it's for (what it controls) | When you touch it |
|---|---|---|
default-src | Fallback value: applies to any resource type that doesn't have its own specific directive below. | Almost never — it's a safety net |
script-src | Where JavaScript can be loaded and run from (<script>, code in onclick attributes, etc.) | When installing a chatbot, a tracking pixel, a reviews widget... |
style-src | Where CSS stylesheets and styles written directly in the HTML can be loaded from | When adding an external font (Google Fonts, Adobe Fonts...) |
img-src | Where images can be loaded from | If you use an image CDN, external avatars (Gravatar), embedded maps... |
font-src | Where font files (.woff, .ttf...) can be loaded from | Almost always goes hand in hand with style-src for the same font service |
connect-src | Which domains the page's JavaScript can connect to (fetch requests, XMLHttpRequest, WebSockets) | When integrating analytics, live chat, any service that sends data in the background |
frame-src | Which external pages can be embedded inside an <iframe> on your site | When embedding a YouTube/Vimeo video, a Google Maps map, a form from another service |
media-src | Where audio/video files played with <audio>/<video> can be loaded from | If you host podcasts or your own video outside your domain (e.g. on a separate CDN) |
object-src | Controls <object>, <embed>, and <applet> (Flash and old browser plugins) | Never — always leave it at 'none', it's obsolete and insecure technology |
base-uri | Which URLs the page can use as a base to resolve relative links (<base> tag) | Almost never — 'self' is correct 99% of the time |
form-action | Which URLs a form on your site can submit (action=) to | If a form sends data to an external domain (e.g. a payment gateway with a redirect) |
frame-ancestors | Who can embed YOUR site inside an <iframe> (anti-clickjacking protection) | Almost never — 'self' already covers most cases |
upgrade-insecure-requests | Forces any resource requested over HTTP to automatically reload over HTTPS (takes no values, stands alone) | Always leave it on if your site uses HTTPS (the norm nowadays) |
🔍 How to Find the Value to Look For When Something Breaks
You have three ways to find out exactly which domain your CSP is missing, from fastest to most thorough:
- Browser console (F12 → "Console" tab): the most direct method. Every blocked resource shows up as a red line literally saying "Refused to load/execute... because it violates the following Content Security Policy directive: 'script-src ...'" — the final part tells you the exact directive, and right before it you'll see the full URL of the blocked resource. Copy that URL's domain and add it to that directive.
- Browser "Network" tab (F12 → "Network"): useful when you want to see, even before enabling CSP, which domains your page actually depends on. Reload the page with that tab open and you'll see every request with its origin domain — so you know in advance what you'll need to authorize.
- The external service's own documentation: if you're installing a live chat, a payment form, or a well-known third-party widget, search its help site for "CSP" or "Content Security Policy" — most serious services (Stripe, Intercom, HubSpot, etc.) publish exactly which domains you need to authorize and in which directive, because it's a very common question among their own customers.
Refused to load the script 'https://widget.chatprovider.com/loader.js' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'". You find script-src in the text field and add the domain: it goes from script-src 'self' 'unsafe-inline'; to script-src 'self' 'unsafe-inline' https://widget.chatprovider.com;. You save, reload the page with F12 open again — if the chat loads and no new related error shows up, it's fixed. If, once loaded, the chat also needs to connect to another domain to send messages, you'll see a second error pointing to connect-src, and you repeat the same process with that directive.
📖 Where to Consult the Full Reference
With the 12 directives in the table above you'll solve virtually any real case on WordPress. If you ever need a more specific directive that isn't here (there are more than 20 in total, some very rarely used outside complex web apps), the official and most complete reference is MDN Web Docs (Mozilla) documentation, which maintains the full list with examples and per-browser compatibility — search "MDN Content-Security-Policy" in any search engine. It's the same source browser developers themselves use.
🔌 Permissions-Policy — Browser API Control
🎯 What it does (simple explanation)
Imagine your website is a house and each visitor comes in with their phone. This permission tells the browser: \"this site doesn't need to know where you are, see your face, or listen to you\". So even if a malicious script sneaks into your site, the browser won't lend it the camera, microphone, or location.
⚠️ Why it matters (real scenario)
Think about this: you have an online store. A hacker finds a flaw in a plugin you installed 2 years ago and manages to inject a script. That script could silently turn on the camera of the visitor browsing your products, pull their exact location, or record with the microphone. Without Permissions-Policy, the browser won't say no. With this enabled, even if the malicious script tries, the browser will respond: \"Sorry, this site doesn't have permission to use the camera\". And the visitor never even knows it was attempted.
🧩 What exactly does it block?
These are phone or computer features that a normal website DOES NOT need to work. If your site is a blog, a store, or a corporate site, you shouldn't need any of these:
📡 Location
Example: A bakery website doesn't need to know where you live to show you a cake. Risk: A spy script could track your visitors. Blocked ✅
🎥 Camera
Example: A recipe site doesn't need to see you to teach you how to cook. Risk: Spying on visitors by recording them without their knowledge. Blocked ✅
🎤 Microphone
Example: A digital newspaper doesn't need to hear you to show you news. Risk: Recording private conversations. Blocked ✅
💳 Payments / 🔔 Notifications
Example: Most sites use their own payment form or third-party plugins, not the browser's native API. Risk: Unauthorized scripts taking money or sending fake notifications. Blocked ✅
🔄 Sensors / 🔗 USB
Example: A news site doesn't need to know if you're walking (accelerometer) or access your USB devices. Risk: Knowing if the user is still or moving, or accessing USB files without permission. Blocked ✅
🔐 SRI (Subresource Integrity) — CDN Resource Protection
🎯 What it does (simple explanation)
Imagine you order a pizza for delivery. The delivery person brings you a sealed box with a security seal. If the seal is broken, you know someone opened it and you don't eat it. SRI does exactly that with the files your site loads from other servers (Google Fonts, jQuery, Bootstrap...): it puts a \"digital seal\" on each file. If the file arrives modified (by a hacker, for example), the browser detects the seal is broken and rejects it. The user never knows, but they're protected.
⚠️ Why it matters (real scenario)
Almost all websites use external resources: Google Fonts for nice typography, jQuery for visual effects, Bootstrap for mobile responsiveness, Google Analytics for visitor stats... All these files are on servers you DON'T control. There have been real cases where hackers injected malicious code into well-known CDNs. Without SRI, if a CDN gets hacked, your site would load that virus without you noticing. With SRI, the browser says: \"This file doesn't match its fingerprint, I'm blocking it\".
⚙️ How it works in 3 steps (easy)
1️⃣ Automatic scan
SeenSecure looks at your site and finds all files loaded from external servers (Google Fonts, jQuery, Bootstrap, icons, etc.). It's like taking inventory of everything that comes into your house.
2️⃣ Fingerprint is calculated
For each file, SeenSecure generates a unique \"digital fingerprint\" (a special code that identifies that exact file). It's like taking fingerprints of each resource. If the file changes even by one letter, the fingerprint changes completely.
3️⃣ Active protection
When someone visits your site, the browser compares the fingerprint of the file that arrives with the one it should have. If they match → the file runs normally. If they don't match → the file is rejected. Simple as that. The visitor sees the site correctly and never knows there was an attack attempt.
📋 Summary & Recommendations
Not all headers have the same level of risk or urgency. This table helps you prioritize:
Enable Immediately
These headers have no negative side effects and protect against common attacks:
- ✅ X-Frame-Options: DENY — Clickjacking
- ✅ X-Content-Type-Options: nosniff — MIME sniffing
- ✅ Referrer-Policy: strict-origin-when-cross-origin
- ✅ Permissions-Policy — Browser APIs
- ✅ SRI — CDN resource integrity
Enable with Caution
These headers require prior verification. They can block the site if misconfigured:
- ⚠️ HSTS — Start with a low max-age (3600). Only raise to 2 years when you are 100% sure HTTPS will never be disabled.
- ⚠️ CSP — Do not enable it with the default one-line value. Build the policy yourself in Monitoring mode first, watching the browser console. If something gets blocked, adjust the policy.