Most web security for an ordinary site comes down to three things: serve everything over TLS, send the right response headers, and validate anything a user can influence. This page covers the headers, the attacks they defend against, and the DNS records that stop people sending mail as you.
Sitecheck checks the headers below on every scan, so this doubles as an explanation of what those findings mean.
HTTPS and TLS
TLS (Transport Layer Security) encrypts traffic between browser and server; HTTPS is HTTP carried over it. "SSL" is the obsolete predecessor — the name stuck, but every current deployment is TLS, and you want TLS 1.2 minimum, TLS 1.3 preferred.
HTTPS provides three things: confidentiality (nobody reads the traffic), integrity (nobody modifies it in flight), and authentication (you are talking to the real server). Without it, any network between the user and you can inject scripts or ads, harvest credentials, or silently rewrite content. Browsers mark plain HTTP as "Not secure", HTTP/2 and HTTP/3 will not negotiate without it, and it is a confirmed — if lightweight — ranking signal.
- Redirect all HTTP to HTTPS with a 301, at the edge.
- Automate renewal. Expired certificates are one of the most common self-inflicted outages, and they take the entire site down at once.
- Restrict issuance with a CAA record so only your chosen CAs can mint certificates.
- Eliminate mixed content — a single
http://script on an HTTPS page is blocked by browsers and breaks the feature it powers.
HSTS — Strict-Transport-Security
HSTS tells the browser to use HTTPS for a domain always, for a stated duration, and to refuse to let users click through certificate warnings.
It closes the gap that a plain redirect leaves open: the very first request, before the redirect, still travels over HTTP and can be intercepted. Once a browser has seen an HSTS header, it upgrades every subsequent request locally and never sends that first plaintext request.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- Start with a short
max-ageand raise it once you are confident. HSTS is hard to undo — browsers honour the cached policy until it expires, so a mistake locks users out of HTTP for that whole duration. - Only add
includeSubDomainswhen every subdomain can serve HTTPS, including internal tools and staging. preloadsubmits your domain to a list baked into browsers. Read the removal process before submitting; getting off the list takes months.
CSP — Content-Security-Policy
CSP declares which sources the browser may load scripts, styles, images, frames, and other resources from. It is the strongest defence available against XSS: even if an attacker injects a <script> tag, the browser refuses to execute it unless the source is allowlisted.
- Deploy with
Content-Security-Policy-Report-Onlyfirst and collect violations. Shipping an enforcing policy blind will break your own site. - Prefer nonces or hashes over
'unsafe-inline'. A policy containing'unsafe-inline'for scripts provides close to no XSS protection, which is the single most common way CSP gets deployed uselessly. - Set
object-src 'none'andbase-uri 'self'— cheap, and they close real bypasses. - Use
frame-ancestorsto control framing; it supersedesX-Frame-Options.
Clickjacking and frame-ancestors
Clickjacking loads your site in a transparent iframe over attacker-controlled UI, so the victim thinks they are clicking one thing while actually clicking a button on your page — approving a payment, changing a setting, granting a permission.
The fix is to refuse to be framed:
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: DENY does the same job for older browsers. Send both if you need the coverage; frame-ancestors wins where supported and is the one that accepts a list of permitted origins.
XSS — Cross-Site Scripting
XSS is when attacker-controlled input is rendered as executable script in another user's browser. Stored XSS persists it in your database; reflected XSS bounces it off a URL parameter; DOM-based XSS never touches the server at all.
The impact is total for the affected user: session theft, keylogging, silent actions performed as them, credential harvesting on a page they correctly believe is yours.
- Escape on output, contextually — HTML, attribute, JavaScript, and URL contexts each need different escaping. Escaping on input is not a substitute.
- Never build DOM with
innerHTMLfrom user data. UsetextContent, or sanitise with a maintained library. - Modern frameworks escape by default; the vulnerabilities come from the deliberate escape hatches (
v-html,dangerouslySetInnerHTML). - Layer CSP behind all of it, so a mistake is not automatically exploitable.
CSRF — Cross-Site Request Forgery
CSRF tricks a logged-in user's browser into making a state-changing request to your site from a page they did not expect — the browser helpfully attaches their cookies, so the request looks authentic.
- Set
SameSite=Lax(orStrict) on session cookies. This alone stops most classic CSRF. - Use anti-CSRF tokens for state-changing requests, tied to the session and verified server-side.
- Never allow
GETto change state. AGETthat deletes something can be triggered by an<img>tag. - Check
Origin/Refereron sensitive endpoints as defence in depth.
SQL injection
SQL injection is when user input is concatenated into a query, letting an attacker alter its structure — reading arbitrary tables, bypassing authentication, or destroying data.
The fix has been the same for two decades and it works: parameterised queries. Never build SQL by string concatenation, and do not try to sanitise your way out of it.
- Use bound parameters or a query builder/ORM that emits them.
- Apply least privilege to the database user. An application account rarely needs
DROP. - Validate types and ranges at the boundary as a second layer.
- Remember injection is not only SQL — the same shape of bug exists in NoSQL queries, shell commands, and LDAP filters.
CORS — Cross-Origin Resource Sharing
CORS is a browser mechanism that relaxes the same-origin policy, letting a page on one origin read responses from another when the server opts in via Access-Control-Allow-Origin.
It is worth being clear about what CORS is not: it is not a server-side access control. It constrains what browser JavaScript may read; it does nothing to stop a direct request from curl or a server. Authentication and authorisation still have to be enforced on the endpoint.
- Allowlist specific origins. Never reflect the request
Originheader back unconditionally — that defeats the entire mechanism. Access-Control-Allow-Origin: *cannot be combined with credentials, and that restriction exists for a good reason. Do not work around it.- Keep preflight (
OPTIONS) responses cacheable withAccess-Control-Max-Ageto avoid a latency hit on every call.
Referrer-Policy
Referrer-Policy controls how much of the current URL is sent in the Referer header when a user navigates away or loads a resource.
The default leaks more than most people expect: full URLs, including paths and query strings that may carry tokens, search terms, or internal identifiers, are sent to third parties.
strict-origin-when-cross-origin is the sensible default — full URL for same-origin requests, origin only cross-origin, nothing at all when downgrading to HTTP. Use no-referrer on pages with genuinely sensitive URLs.
Permissions-Policy
Permissions-Policy (formerly Feature-Policy) declares which browser features the page and its iframes may use — camera, microphone, geolocation, payment, and others.
Its main value is containment: it stops a compromised third-party script or embedded iframe from silently requesting powerful capabilities. Disable everything you do not use:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Email authentication — SPF, DKIM, DMARC
These three TXT records stop other people sending mail as your domain. They work together and are close to useless alone.
SPF lists the servers permitted to send mail for your domain. Publish exactly one SPF record, and stay under the RFC 7208 limit of 10 DNS lookups — each include: can nest, and exceeding the limit makes SPF fail.
DKIM cryptographically signs outgoing messages with a private key, and receivers verify against a public key in DNS. Use a separate selector per sending provider so you can rotate one without touching the others.
DMARC ties the two together: it tells receivers what to do when SPF and DKIM fail, and where to send reports.
v=DMARC1; p=none; rua=mailto:dmarc@example.com
Roll out in that order, and start at p=none. That is a monitoring mode: it changes nothing about delivery but starts the reports flowing. Read them until you are confident every legitimate sender — your CRM, invoicing system, marketing platform, helpdesk — passes. Only then move to p=quarantine, and later p=reject.
Jumping straight to p=reject is the classic mistake. It silently blackholes legitimate mail from systems you forgot you had, and you find out when a customer mentions they never got an invoice.