Website security for small businesses: where to start
You are not targeted. You are found.
Nobody wakes up determined to break into a twelve-person company's booking form. What actually happens is duller: automated scanners sweep the whole internet and every certificate transparency log looking for a known flaw in a known version, and your site turns up in that sweep exactly like everyone else's.
Two figures from public sources are worth holding on to. France's national CERT reports in its 2025 cyber threat overview (p. 9) that small and mid-sized companies made up 48% of the ransomware victims it handled in 2025, against 37% in 2024 — by far the largest category. And the 2026 Verizon Data Breach Investigations Report puts software vulnerabilities at the start of 31% of breaches, up 20% on last year and now ahead of stolen passwords.
There is good news buried in that. The openings being used are boring ones, and nothing in the first half of this article needs a security team.
An order that holds up
Sort by what an attacker gets for the least effort, not by what sounds frightening. Three questions, in this order:
- If this one thing falls, does everything else fall with it? (Domain registrar, DNS, email.)
- Can a script find it without knowing anything about you? (Old versions, exposed files, missing headers.)
- If it happens anyway, can you come back? (Backups, logs.)
WAFs, certifications and the vendor you met at a trade show all come after those.
One rule before everything else: what follows applies to a site you own, or one you hold written authorisation to test. Probing someone else's application without their agreement is a criminal offence (in France, article 323-1 of the Penal Code). And if the site runs on shared hosting or a SaaS platform, check their terms too: several require advance notice before an active scan.
Level 0 — the afternoon that costs nothing
MFA on admin accounts, registrar first
Start with the domain registrar and the DNS provider, not the CMS. Whoever controls DNS can point your mail elsewhere, reissue TLS certificates, and then trigger a password reset on every other service you own. It is the single account from which everything else can be taken.
Then, in order: hosting/cloud console, email tenant, source control, CI/CD, payment provider, admin back office.
Use a TOTP app or a hardware key. Avoid SMS where you have the choice — SIM swapping is a documented, routine technique, not a theoretical one. The OWASP MFA cheat sheet is the reference if you need to argue the point with someone. While you are there, query Have I Been Pwned: domain search asks you to prove the domain is yours first, and becomes paid above 25 addresses, but it will tell you whether your users' credentials are circulating. The ones that leak from an unrelated site get replayed against yours.
Remove the people who left
Go service by service, not person by person — you will forget someone otherwise. For each: user list, API tokens, SSH keys, OAuth apps, webhooks, shared mailboxes, and recovery phone numbers still pointing at an old handset. The classic oversight is the shared account whose password was never renewed: one departure and the credential is now outside the company forever.
Update dependencies, and triage properly
The commands, per ecosystem:
npm audit— docspip-audit— PyPIcomposer audit— Composer CLIosv-scanner scan source -r .— OSV-Scanner, which covers most ecosystems at once; without-rit does not descend into subdirectories
Turn on Dependabot alerts so this happens without you remembering.
Then triage, because the raw output will scare you for no reason. A critical CVE in a build-time dev dependency is not the same as a medium one in the library that parses uploaded files. Two filters do most of the work: is the vulnerable code path actually reachable from user input, and is the CVE in the CISA Known Exploited Vulnerabilities catalog? Anything on that list is being used in the wild right now — fix those first, regardless of score.
Security headers, exactly
These are configuration, not code. Most are one line in nginx, Apache, or your CDN.
| Header | Value | What it buys you |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | The browser refuses plain HTTP to your domain for a year. Kills downgrade interception on hostile Wi-Fi. |
Content-Security-Policy | default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self' | Limits what an injected script can load or exfiltrate, stops your pages being framed for clickjacking, and stops an injected form posting your data somewhere else. |
X-Content-Type-Options | nosniff | Stops the browser guessing content types — an uploaded file served as text won't be executed as script. |
Referrer-Policy | strict-origin-when-cross-origin | Locks in what Chrome, Firefox and Safari already do by default, including on browsers that don't. Matters if you ever put a token in a URL parameter. |
Permissions-Policy | geolocation=(), camera=(), microphone=() | Denies browser capabilities you don't use, including to embedded third-party content. |
Set-Cookie (session) | Secure; HttpOnly; SameSite=Lax | Session cookie never travels over plain HTTP and is invisible to JavaScript. Lax blocks it on cross-site POSTs and sub-resource requests, but still sends it on top-level GET navigations — use Strict if nothing depends on inbound links landing logged-in. |
Two footnotes. frame-ancestors 'none' supersedes X-Frame-Options in current browsers; keep X-Frame-Options: DENY alongside it only if you still support very old clients. And for X-XSS-Protection, the OWASP Secure Headers Project recommends setting X-XSS-Protection: 0 rather than removing the header: an absent header leaves the filter at its default behaviour on older browsers, whereas 0 disables it explicitly.
A third footnote, the most expensive one to miss. includeSubDomains applies to every subdomain you have, including any still served over HTTP, and a browser that has cached max-age=31536000 will refuse them for a year with no way for you to undo it remotely. Inventory your subdomains first, then ramp up in steps — max-age=300, then 86400, then a year — and only add includeSubDomains once everything answers over HTTPS.
CSP is the one that breaks things on the first attempt. Deploy it as Content-Security-Policy-Report-Only first, watch what the console complains about for a week, then enforce.
Verify your work for free: MDN HTTP Observatory grades the whole set, CSP Evaluator tells you whether your policy actually stops anything, SSL Labs or testssl.sh covers TLS, and hstspreload.org is the next step once HSTS has been stable for a while. Certificates themselves are free and automatable via Let's Encrypt; there is no reason left for an expired one.
A backup you have actually restored
A backup that has never been restored is a hypothesis. Test it: restore to a clean machine, time it, and check that both the database and user-uploaded files came back. Two properties matter — the backups must live in a separate account with separate credentials, and they must be immutable or offline. Ransomware operators look for the backup job's credentials precisely because they are usually stored next to everything else.
Level 1 — the following week
Split your accounts. Daily work does not happen under an admin login. The application should not connect to its database as superuser; a role that cannot DROP TABLE limits what a successful injection achieves.
Keep logs, for 90 days minimum. Who logged in, from where, what changed. Without them, after an incident, you cannot answer the only question that matters — what left? — and that is also the question a regulator asks.
Delete what's lying around. Two lines of shell, to run against your own site — replace SITE with your own domain before you press enter:
SITE="https://your-domain.example"for p in /.git/config /.env /backup.sql /phpinfo.php /server-status /.DS_Store; do curl -s -o /dev/null -w "%{http_code} $p\n" "$SITE$p"; done
Anything that is not 404 or 403 deserves five minutes of attention. /.git/config returning 200 means your entire source history, including whatever secrets were committed in 2021, is downloadable.
Rotate secrets that were ever committed. Deleting the line does not help — it is still in the history. gitleaks will find them; rotation is the only fix.
Test access control by hand. This is the highest-value thing on the list and no scanner will do it for you. Create two accounts. As account A, find an identifier in a URL or an API response — /api/invoices/1042. Log in as B and request the same resource with B's session: curl -H "Cookie: session=<B's session>" "$SITE/api/invoices/1042".
If B sees A's invoice, you have Broken Object Level Authorization — first on the OWASP API Security Top 10. Repeat on every endpoint taking an id, uuid, slug or filename, and on PDF exports and file downloads especially, which are usually written after the permission checks were designed. A scanner cannot find this because it has no idea that 1042 belongs to A.
Level 2 — free tools you can point at yourself
ZAP will crawl and actively test an application. Nuclei checks thousands of known-issue templates quickly. WPScan is the one to use for WordPress. Run active scans against staging, or against production only with a fresh backup and outside business hours — an active scanner submits forms.
Expect noise. Scanner output is a list of candidates, not findings; every item needs reproducing by hand before it means anything. That is precisely what separates a tool export from a security report worth the name.
When outside eyes start paying for themselves
Three things you will not find on your own site, however competent you are:
- Business logic. There is no signature for "this workflow can be short-circuited" — the worked examples are in the pricing article.
- Chains. A version number leaked here, a forgotten endpoint there, a permission checked on the page but not on the API. Individually low. Together, an account takeover.
- Author bias. You do not test what you are confident you implemented correctly. That is not a skill problem; it is why code review exists.
Concrete triggers for bringing someone in: you hold personal data beyond a contact email, you handle money, you run multi-tenant (each customer sees their own data), you just shipped a rewrite or a public API, or a client sent you a security questionnaire in writing.
And a genuine reason to wait: paying for an audit of a site with no MFA and an untested backup means buying a report whose first page you could have written yourself. Do Level 0 first.
The checklist
- [ ] MFA on registrar, DNS, hosting, email, source control, CI/CD, payments, admin — TOTP or hardware key
- [ ] Departed staff and ex-contractors removed from every service, including tokens, SSH keys and webhooks
- [ ] Dependency audit run; anything in the CISA KEV catalog patched
- [ ] Automated dependency alerts enabled
- [ ] Six headers set; verified with HTTP Observatory and CSP Evaluator
- [ ] TLS valid, auto-renewing, graded with SSL Labs
- [ ] Backup restored onto a clean machine, timed, files included; stored in a separate account, immutable or offline
- [ ]
.git,.env,.sql,phpinfonot reachable from the web - [ ] Secrets ever committed have been rotated
- [ ] Two-account ID-swap test run across every endpoint with an identifier
- [ ] Auth and admin logs retained 90 days
- [ ] Application's database user is not superuser
Most of that is an afternoon and a follow-up morning. It removes the openings that scanners find — the ones automated sweeps try first.
If you would rather have someone else look, and you want to know what that work costs elsewhere before deciding, here is the offer, exactly.
That is what iwantreport.dev sells: one web application and its API, tested against the OWASP Top 10, the API Security Top 10, the CWE Top 25 and the OWASP ASVS — a scanner on the first pass, then every result reproduced by hand before it enters the report. Written report within 48 hours, one re-test included, 200 €, 30-day money-back guarantee: just ask by email within 30 days of delivery. Out of scope: internal network, source code review, phishing and social engineering, load or denial-of-service testing, and mobile applications.