Security basics every small business site needs

HTTPS is table stakes. The short list that actually reduces “we got hacked” Mondays: updates, backups, least privilege, forms, headers and host hygiene.

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.php and 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: nosniff
  • Referrer-Policy sensible default
  • Permissions-Policy to 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

  1. Take a calm forensic snapshot if needed (don’t blindly delete evidence)
  2. Rotate credentials and keys
  3. Patch the hole
  4. Restore from known-good backup if integrity is uncertain
  5. Review how it happened; fix process, not only files
  6. 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 →