Security report
Locked down, and proven by tests
The portals I lead hold account, payment and contract data for councils and their business customers, so security is part of the build, not a review at the end. This page shows how this site is locked down, then the practices I apply on client work.
- Content policy
- No inline scripts or styles
- Third-party requests
- None
- Pinned by
- 26 automated tests
- Last reviewed
- October 2026
What this site sends with every page
Open the network tab in your browser's developer tools and you can check each of these yourself. They are applied to every response, error pages included.
| Header | Setting | What it stops |
|---|---|---|
| Content-Security-Policy | Same-origin only, with a fresh nonce per request. No unsafe-inline, no unsafe-eval |
Injected scripts and styles cannot run, even if one made it into the page |
| X-Frame-Options | DENY, backed by frame-ancestors 'none' |
Clickjacking: the site cannot be embedded in someone else's page |
| X-Content-Type-Options | nosniff |
Browsers guessing a file type and running it as something it is not |
| Referrer-Policy | strict-origin-when-cross-origin |
Full page addresses leaking to other sites |
| Permissions-Policy | Location, camera, microphone, payment and USB all off | Any script asking for device access the site never needs |
| Cross-Origin-Opener-Policy | same-origin |
Other sites keeping a handle on this window after opening it |
| Strict-Transport-Security | One year, subdomains included, sent over HTTPS | Downgrade to an unencrypted connection |
How this site earns a strict policy
- Nothing inline. No
onclickhandlers and nostyleattributes anywhere in the markup. A test fetches every page and fails the build if one appears. - Nothing third-party. Fonts are self-hosted and there are no analytics, trackers or CDN scripts, so there is no outside supplier to compromise.
- One cookie. The session cookie is HTTP-only and same-site, and every form carries a CSRF token.
What I do on client portals
These are practices from the portals I lead at work. The code belongs to my employer and its clients, so they are described here, not shown.
- Every ID is checked for ownership. I audit every endpoint that accepts an ID and record it in one table: where the ID comes from and which check guards it. The checks fail closed, so a record with no owner is refused, not waved through.
- Forbidden looks like not found. A visitor probing for an admin area gets the same 404 as for a page that does not exist, whether the refusal came from a thrown exception or a controller's own response.
- A strict content policy on a real application. On one portal I moved every inline handler and style attribute out of the views so the policy could drop
unsafe-inlineandunsafe-evalcompletely, with a per-request nonce for the one block that is generated per tenant. - Rate limits that survive a spoofed address. Behind a cloud load balancer the client IP header cannot be fully trusted, so login and password reset limits are keyed on the submitted email as well as the IP.
- Rules are checked live, not from the session. A business rule that gates an action is checked again against the back-office system when the action is taken, and the request is refused if that lookup fails.
- Roles are enforced twice and tested exhaustively. Routes block the request and views hide the control, both from one source of truth. Unit tests cover every combination of roles, and a route-by-role matrix proves who can reach what.
- Admin switches are a ceiling, not a floor. Turning a feature off in the admin panel removes it for everyone. Turning it on grants nothing by itself: role checks still apply.
- Impersonation leaves a trail. Support staff can see the portal exactly as a customer does without knowing their password. The admin's own session is set aside and restored afterwards, and every use is logged.
- Payments are watched for odd behaviour. A monitor flags a payment session whose IP address changes part-way through, and status checks that are repeated, duplicated or too frequent.
- Exports are safe to open. Free text written to a spreadsheet export is neutralised so a value starting with
=cannot run as a formula. - Secrets stay out of sight. Debug output is tied to the local environment, so a stray setting on a live server cannot expose stack traces, and submitted passwords are never written back into the session after a failed attempt.
- Every security change is written down. A security changelog records the severity, who could have exploited it, what changed, and the named test that stops it coming back.
Known limitations
- No external scan yet. The headers are verified by this site's own tests. An independent scan of the live site is still to be run and published here.
- No HSTS preload. The header is sent, but the domain has not been submitted to the browser preload list.