Most small business breaches aren’t Hollywood zero-days. They’re outdated plugins, reused passwords, open admin URLs, missing backups, and hosts that treat security as optional.
This is the boring checklist that works — technical detail included, jargon explained. You don’t need a security theatre binder. You need hygiene, least privilege, and a restore plan you’ve practised.
The hygiene list that prevents most incidents
- Updates — core, plugins, themes, PHP version; test on staging when you can
- Unique admin credentials + 2FA where available
- Least privilege — not everyone is Administrator
- Off-site backups you’ve restored at least once
- SSL everywhere; HSTS when you’re ready and stable on HTTPS
- Form spam protection that doesn’t punish real humans (and still rate-limits bots)
- File permissions and no leftover install scripts / phpinfo pages
Security is mostly hygiene plus a restore plan.
Authentication and accounts
- Long unique passwords or a password manager — shared “Admin123” is an open door
- Change default admin usernames where the platform allows
- Limit login attempts; watch for brute force on
wp-login.phpand custom logins - Session timeouts appropriate to the sensitivity of the data
- Revoke access the same day someone leaves the company
For client portals, regenerate session IDs on login and use cookie flags that match a modern baseline: Secure, HttpOnly, and a sensible SameSite policy. Store password hashes with password_hash() (or your framework’s equivalent) — never reversible encryption “so support can look them up”.
WordPress-focused hardening (still relevant)
- Delete unused themes/plugins — attack surface you don’t use still gets scanned
- Keep the number of plugins low and reputable
- Disable file editing from the dashboard in production
- Restrict XML-RPC if you don’t need it
- Principle of least privilege for editors vs admins
- Keep automatic updates on for trusted minor patches where your process allows
// wp-config.php ideas (illustrative)
define('DISALLOW_FILE_EDIT', true);
// move keys/salts to environment where possible
Security plugins can help with scanning and login limits, but they are not a substitute for updates and backups. A locked door on a building with no roof still gets rained on.
Application-level musts (any stack)
- Prepared statements / parameterised queries
- Output escaping to prevent XSS
- CSRF tokens on state-changing forms
- Safe file uploads (type, size, storage location)
- No secrets in front-end JavaScript or public repos
- Server-side authorisation on every sensitive action (hiding a button is not security)
// XSS-safe output in PHP
echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
// PDO prepared statement
$stmt = $pdo->prepare('UPDATE users SET email = ? WHERE id = ?');
$stmt->execute([$email, $id]);
File uploads deserve paranoia: validate MIME and extension carefully, store outside the web root when possible, serve via a controlled endpoint, and never execute user content as code.
HTTP security headers (practical set)
Not magic, but useful layers:
Strict-Transport-Security(after HTTPS is solid)Content-Security-Policy(start report-only if needed)X-Content-Type-Options: nosniffReferrer-Policysensible defaultPermissions-Policyto lock unused browser features
Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; \
script-src 'self'; style-src 'self' 'unsafe-inline'
CSP is powerful and easy to break with inline scripts and third-party tags. Introduce it carefully; don’t copy a strict policy from a blog post onto a tag-manager-heavy marketing site without testing.
Backups that count
A backup you have never restored is a rumour. Define:
- What is backed up (files + database)
- How often
- Where (off-site, not only the same VPS)
- Retention
- Who runs a restore test quarterly
Ransomware and bad deploys both become survivable when you can rebuild. Keep at least one backup that is not writable by the live application user.
Hosting matters
A cheap host with noisy neighbours, no malware scanning, outdated PHP and opaque support is a false economy. Pay for a host that matches the value of the leads or orders the site generates. UK businesses often benefit from UK/EU data residency and support in a sensible timezone.
- Supported PHP versions (not end-of-life)
- SSH access and staging environments
- Malware scanning / WAF options you understand
- Clear ownership of who applies patches
Forms, spam and abuse
Contact forms are both a lead channel and an attack surface:
- Rate-limit submissions by IP and by form
- Use honeypots and/or proof-of-work lightly; CAPTCHAs as a last resort for UX
- Validate server-side; never trust client-only checks
- Don’t echo raw user input into admin emails without escaping
- Store only what you need from each enquiry
Dependencies and supply chain basics
- Prefer well-maintained packages with a clear owner
- Run
composer audit/ npm audit equivalents before major releases - Pin versions; deploy lockfiles
- Remove abandoned plugins instead of “it still works”
Incident basics
- Take a calm forensic snapshot if needed (don’t blindly delete evidence)
- Rotate credentials and keys
- Patch the hole
- Restore from known-good backup if integrity is uncertain
- Review how it happened; fix process, not only files
- Communicate clearly with stakeholders — silence destroys trust
Write a one-page incident note while the details are fresh: timeline, impact, root cause, preventive actions. Future you (and your clients) will need it.
GDPR-minded hygiene (practical, not legal advice)
- Collect less personal data by default
- Know where enquiries and logs live
- Have a path to export or delete personal data when required
- Limit admin access to who genuinely needs it
Security and privacy overlap: fewer copies of customer data means fewer places a mistake can hurt people.
Worked examples (security)
Concrete patterns for PHP/WordPress-style stacks — adapt names to your codebase.
1) Prepared statements only
// Never:
// $sql = "SELECT * FROM users WHERE email = '$email'";
// Always:
$stmt = $pdo->prepare('SELECT id, role FROM users WHERE email = ? LIMIT 1');
$stmt->execute([$email]);
$user = $stmt->fetch();
2) Escape every string that hits HTML
function h(?string $value): string {
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo '<p>Hello, ' . h($name) . '</p>';
3) CSRF on POST forms
// Session already started
$token = $_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// Form field:
// <input type="hidden" name="csrf" value="<?= h($token) ?>">
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
http_response_code(403);
exit('Forbidden');
}
}
4) Safe file upload sketch
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['doc']['tmp_name']);
$allowed = ['application/pdf' => 'pdf', 'image/jpeg' => 'jpg'];
if (!isset($allowed[$mime])) {
throw new RuntimeException('File type not allowed');
}
if ($_FILES['doc']['size'] > 5 * 1024 * 1024) {
throw new RuntimeException('File too large');
}
$ext = $allowed[$mime];
$name = bin2hex(random_bytes(16)) . '.' . $ext;
// store outside public web root when possible
move_uploaded_file($_FILES['doc']['tmp_name'], $storageDir . '/' . $name);
5) Useful security headers (Apache-style idea)
# After HTTPS is stable
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
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=()"
6) WordPress: disable file editor in production
// wp-config.php
define('DISALLOW_FILE_EDIT', true);
define('FORCE_SSL_ADMIN', true);
Want a security + backup review? Book a free call →