Securing a small business website: headers, forms, and updates
A small business website is rarely attacked because someone chose you. It is attacked because automated tools scan the whole internet for common weaknesses, and your site was on the list. That is good news: protecting against automated, opportunistic attacks mostly means getting a few basics right.
This post covers three of them. Security headers, which take minutes. Forms, which are where most input from strangers arrives. And updates, which are where most small sites actually fail.
1. Security headers
Security headers are instructions your server sends to the browser with every page. They cost nothing to add and block several classes of attack outright. The OWASP HTTP Headers Cheat Sheet is the best single reference.
The headers to set
| Header | Suggested value | What it does |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | Browsers always use HTTPS for your site. |
Content-Security-Policy | Start with default-src 'self' and adjust | Limits where scripts, styles and other resources may load from. |
X-Content-Type-Options | nosniff | Stops browsers guessing file types. |
Referrer-Policy | strict-origin-when-cross-origin | Doesn't leak full URLs to other sites. |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Turns off browser features you don't use. |
X-Frame-Options | DENY (or CSP frame-ancestors 'none') | Stops other sites framing yours (clickjacking). |
HSTS, carefully
Strict-Transport-Security tells browsers to use HTTPS only. MDN's HSTS page explains why it matters: without it, the very first visit over plain http:// can be intercepted before your redirect to HTTPS happens. Two cautions. includeSubDomains applies to every subdomain, so make sure they all work over HTTPS first. And once browsers have seen the header, they will refuse plain HTTP until max-age runs out, so start with a short value (say, max-age=300) and increase it once you are sure.
CSP, gradually
A Content-Security-Policy is the most powerful header and the one most likely to break something, especially on sites with plugins, chat widgets or embedded videos. Deploy it first as Content-Security-Policy-Report-Only, which reports problems in the browser console without blocking anything, fix what it reports, then switch to enforcing.
Headers to remove
OWASP recommends removing X-Powered-By, which tells attackers which technology you run. It also recommends not setting the old X-XSS-Protection header, because in some cases it can create vulnerabilities in otherwise safe sites; use a CSP instead.
Check your headers
You can see your headers in your browser's developer tools (Network tab, click the page, look at Response Headers). Or use this short Python script, which needs only the standard library:
import sys
import urllib.request
EXPECTED = [
"content-security-policy",
"strict-transport-security",
"x-content-type-options",
"referrer-policy",
"permissions-policy",
]
REMOVE = ["x-powered-by", "x-xss-protection"]
url = sys.argv[1]
req = urllib.request.Request(url, method="HEAD")
with urllib.request.urlopen(req, timeout=10) as resp:
headers = {k.lower(): v for k, v in resp.headers.items()}
for name in EXPECTED:
print(("OK " if name in headers else "MISSING ") + name)
for name in REMOVE:
if name in headers:
print("REMOVE " + name + ": " + headers[name])
Save it as check_headers.py and run python3 check_headers.py https://yoursite.example/. Every line should say OK, and no REMOVE lines should appear. The security headers generator writes the configuration for Cloudflare Pages, nginx, Apache or Caddy.
2. Forms
Every form on your site, whether contact, booking, newsletter or login, accepts input from anyone on the internet. Treat that input as untrusted.
Validate on the server
The OWASP Input Validation Cheat Sheet is blunt: client-side validation is bypassable; use it for immediate feedback only. Whatever checks you put in the browser, repeat them on the server:
- Accept only the fields you expect. Reject anything else.
- Set a maximum length for every field.
- Check formats: a real-looking email address, a date that is a date.
- Strip line breaks from anything that ends up in an email header, such as the subject or "reply-to". Otherwise someone can inject extra headers and use your form to send spam.
- Send notification emails as plain text, not HTML built from user input.
Stop spam without annoying people
- A honeypot field: a field hidden from people but visible to simple bots. If it is filled in, quietly discard the message.
- Rate limiting: limit how many submissions one address can make per minute. Most hosts and CDNs offer this.
- A challenge if spam continues. Cloudflare Turnstile, for example, is described by Cloudflare as a CAPTCHA alternative that can work without showing visitors a puzzle.
Don't collect what you don't need
Every field you collect is data you have to protect. A contact form needs a name, an email address and a message. Ask for more only if you will use it.
Keep secrets out of the page
API keys for email services, payment providers or databases belong on the server, in environment variables. Never in JavaScript sent to the browser, and never committed to your code repository.
3. Updates
For sites built on a CMS such as WordPress, outdated software is the most common way in. Automated tools look for known-vulnerable versions of the CMS, its plugins and its themes.
CISA publishes a Known Exploited Vulnerabilities catalog, which it describes as the authoritative source of vulnerabilities that have been exploited in the wild. You don't need to read it daily, but it shows why patching matters: these are flaws attackers are already using.
A routine that works
- Turn on automatic updates for minor and security releases, if your platform supports them.
- Check weekly for plugin and theme updates. Apply them after taking a backup.
- Remove what you don't use. A deactivated plugin can still be vulnerable if its files are on the server. Delete it.
- Choose fewer, well-maintained plugins. Check when a plugin was last updated before installing it.
- Keep the server current too: PHP or Node.js versions, and the operating system, if you manage it.
- Back up before updating, and test that you can restore.
Or avoid the problem
A static site, pre-built HTML files with no server-side code or database, has far less to update. If your site is mostly information pages, it may not need a CMS at all. I wrote about how this site is set up on Cloudflare Pages.
Accounts: the fourth basic
None of this helps if someone logs in to your hosting, domain registrar or CMS admin with a stolen password. Use a password manager, a unique password and two-factor authentication on each of those accounts. See personal security you can finish in a weekend.
A one-page checklist
- HTTPS everywhere, with HSTS.
- Security headers set, and checked with the script above.
- A CSP, tested in report-only mode first.
X-Powered-Byremoved.- Every form validated on the server, with length limits and a honeypot.
- Rate limiting on form endpoints.
- No secrets in front-end code.
- Automatic updates on; weekly check for the rest.
- Unused plugins and themes deleted.
- 2FA on hosting, domain and admin accounts.
This won't stop a determined, targeted attacker. It will stop most of the automated attacks small business sites actually face.
Sources
- OWASP, HTTP Headers Cheat Sheet. Last verified 2026-10-13.
- MDN Web Docs, Strict-Transport-Security. Last verified 2026-10-13.
- OWASP, Input Validation Cheat Sheet. Last verified 2026-10-13.
- Cloudflare Docs, Turnstile. Last verified 2026-10-13.
- CISA, Known Exploited Vulnerabilities Catalog. Last verified 2026-10-13.