External security assessment

Five days of testing against the IamVERA demo environment

An authorised, non-destructive security assessment was carried out over five consecutive days. No critical or high-severity vulnerabilities were identified, and thirty-eight distinct security controls were independently verified as effective. Six findings were reported. All six were worked through on the day the assessment closed: five are remediated and live, and the sixth could not be reproduced. The remediation log below records what changed and why.

Assessment type
Authorised external security testing (grey-box), demo environment
Testing period
23–27 September 2026, five consecutive days
Tester
Daniel Kostovski
Scope
Web application security, authentication flows, session management, authorisation, API endpoints, file handling, TLS configuration, CORS, HTTP methods, subdomain enumeration
Status
Complete — all planned testing activities executed
0Critical or high
1Medium finding
5Low / info
0Still open
38Controls verified

What was tested

More than fifty endpoints, 27,678 directory paths, twenty-one high-value TCP ports, the complete TLS configuration and twenty-three subdomains were examined using manual and low-rate automated techniques.

Testing covered external reconnaissance, HTTP security headers, authentication and password-reset flows, session management and logout invalidation, object-level authorisation (IDOR), privilege escalation and mass assignment, stored and reflected XSS including file-upload vectors, authenticated directory enumeration, CORS preflight handling, Host header injection, HTTP method enumeration, TLS protocol and cipher support, CSRF protection, two-factor authentication enforcement, GraphQL discovery, error handling, null byte injection, open redirect, and subdomain reconnaissance.

All activity was identification only. Nothing was exploited, rate-limit probing was capped at five consecutive attempts per endpoint, IDOR testing used only the tester's own identifiers, and automated scanning was terminated early when the server showed any sign of slowdown. No service disruption occurred at any point.

Findings and status

The severity of each finding below is the assessor's. The remediation status beside it is ours, recorded after the assessment closed and not yet confirmed by a re-test. Where a finding is marked remediated, that is IamVERA stating what it changed, not the assessor stating that the change works.

Medium Remediated

F-01 — No lockout or progressive delay on authentication endpoints

POST /api/auth/login · POST /api/auth/forgot-password

Five consecutive failed login attempts returned identical 401 responses with no 429, no Retry-After, and no lockout behaviour. Five consecutive password-reset requests all returned 202 without throttling. The practical risk is credential stuffing against known email addresses, and inbox flooding through the reset endpoint.

Remediated 27 September 2026. Request-volume limits were already in place and correctly wired, which is why five attempts triggered nothing — they stayed below every threshold. What was missing was a brake on patient guessing rather than on volume. Failed attempts are now counted separately: progressive delay begins at the third consecutive failure and a temporary lockout applies from the eighth. A successful login clears the counter. The lockout key combines the email address with the source network rather than the address alone, so an attacker locks out only themselves and cannot deny service to a known user. RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset are now returned on rate-limited routes, so the policy is observable instead of guesswork. A re-test is scheduled.

Low / info Remediated

F-03 — Backend framework disclosed in response headers

Every response carried x-powered-by: Express, which helps an attacker fingerprint the stack and select framework-specific exploits.

Remediated 27 September 2026. The header is now disabled at the application layer.

Low Not reproduced

F-06 — Deprecated TLS versions reported as accepted

The assessment reported that the server accepts TLS 1.0 and TLS 1.1, deprecated by RFC 8996 and prohibited by PCI DSS 4.0 and NIST SP 800-52 Rev. 2.

We could not reproduce this. Every virtual host on the platform is configured with ssl_protocols TLSv1.3; and nothing else, and external probing confirms that TLS 1.0, 1.1 and 1.2 are all refused while 1.3 negotiates normally. The finding as written also contradicts itself: it states that TLS 1.2 is disabled while 1.0 and 1.1 are enabled, which is not a configuration anyone would produce. The most likely explanation is that a handshake failure was read as a success. We have asked the assessor to re-check this item, and we are leaving it listed rather than removing it, because a reader deserves to see the disagreement rather than a quietly shortened list.

Low Addressed

F-02 — SSH reachable from any source address

Publicly reachable SSH attracts automated brute-force traffic.

Addressed 27 September 2026. Password authentication was already disabled, so password guessing was never possible; only keys are accepted. Reviewing this finding surfaced something the assessment could not see from outside: the intrusion-prevention service was running with no jails configured at all, so it was listed as a control while doing nothing. An SSH jail is now active, with escalating ban durations for repeat offenders. Root login is explicitly set to key-only rather than inheriting that from a separate password setting elsewhere in the configuration.

We deliberately did not restrict SSH to fixed source addresses, which is the other half of the assessor's advice. The people who operate this platform do not work from one address, and a firewall rule that locks out the last remaining administrator is an outage rather than a security control. Keys plus rate limiting is the trade-off we accept, and we would rather state that than quietly mark the item closed.

Low / info Remediated

F-04 — Build metadata exposed in asset version strings

Asset query strings carried a human-readable build date, revision number and an internal feature tag, revealing release cadence and internal naming conventions.

Remediated 27 September 2026. Version strings are now the first ten characters of the file's own SHA-256 digest, generated rather than written by hand. Cache behaviour is unchanged: a browser still refetches the moment the content changes. The same review found the internal build identifier being returned by a public health endpoint, which the assessment had not covered; that has been removed from the response as well.

Informational Remediated

F-05 — A security header is sent twice

x-content-type-options: nosniff was emitted by both the reverse proxy and the application. No functional risk, but it left open which layer owned the header.

Remediated 27 September 2026. The reverse proxy owns it. That is one configuration covering every customer environment instead of the same line repeated in two applications. The applications keep only the headers that depend on the request path and that a proxy cannot decide, such as the document-editing routes that are fetched from a different origin. A test now asserts both halves of that split, because checking only that the application stopped sending it would let the header disappear entirely if it were ever removed from the proxy.

Controls verified as effective

Thirty-eight controls were independently confirmed to work. A selection:

  1. Authorisation holds across tenants. Tampering with session, document and project identifiers returns not-found rather than another customer's data. Project-level access from a different tenant is refused.
  2. Privilege escalation through mass assignment is impossible. Injected role and isCustomerAdmin fields are silently discarded; the project model has no role field to elevate. Verb tampering on read-only endpoints is rejected.
  3. Two-factor authentication is enforced, not optional. Password verification is followed by a FIDO2/WebAuthn passkey step, and protected resources are refused until that step completes.
  4. Sessions are server-side and logout really invalidates. The session cookie is HttpOnly, Secure and SameSite=Lax; replaying it after logout returns 401. Client-side storage holds only non-sensitive interface state.
  5. Uploaded files cannot execute. HTML and SVG payloads with script content are detected and treated as inert data, so stored XSS through uploads does not occur.
  6. Neither login nor password reset reveals whether an account exists. Both return identical responses regardless of the address supplied.
  7. Password reset tokens are unpredictable. Reset identifiers are RFC 4122 v4 UUIDs, resistant to enumeration and forgery.
  8. A hidden support module enforces access control server-side. Directory enumeration found it; supplying ?scope=all as a non-admin is ignored and scope stays restricted to the user's own tickets.
  9. Cross-origin requests and manipulated Host headers are refused. Unauthorised origins receive no CORS headers; forged Host and X-Forwarded-Host values are rejected at the proxy.
  10. The attack surface is small and stays small. Only two ports are reachable; databases, control panels and alternate web ports are filtered. 27,678 enumerated paths found no backups, no legacy scripts, no admin panels and no debug endpoints.
  11. Errors say nothing useful to an attacker. Malformed input returns clean not-found responses with no stack traces or internal paths. Null bytes are rejected at the proxy before reaching the application.
  12. Responsible disclosure is supported. A valid RFC 9116 security.txt is published with contact details, a PGP key reference and an expiry date.

Remediation log — 27 September 2026

Every finding was worked through on the day the assessment closed, and the changes are live across all customer environments. What follows is what changed, how, and why.

Authentication now resists patient guessing, not just volume

The assessment reported that no rate limiting existed. It did: forty requests per quarter-hour per source network, ten per email address. Five attempts stayed under both, which is why nothing triggered and why the policy looked absent.

Those limits count requests. They stop a flood and do nothing about someone patient: nine attempts per quarter-hour is within policy and adds up to more than a thousand guesses overnight. Failed attempts are now counted separately from requests, with a delay that grows from the third consecutive failure and a temporary lock from the eighth. A successful login clears the counter, so ordinary mistyping is unaffected.

The lock is keyed to the email address and the source network together, not the address alone. Locking on address would let anyone shut a known user out of their own account by failing on purpose — trading a brute-force risk for a denial-of-service one. Rate-limit headers are now returned as well, so the policy can be observed rather than inferred.

Two disclosures removed, and one the assessment did not reach

The framework banner is gone, and asset version strings no longer carry a release date, a revision number and an internal feature name. They are now the first characters of the file's own digest, generated rather than written by hand. Caching behaviour is unchanged.

Reviewing that finding turned up the same leak in a place the assessment had not covered: a public health endpoint was returning the internal build identifier in its response body. The assessor had probed a differently named path and received a not-found. That has been removed too. A finding is worth more than the line it was written on, and it is worth re-reading your own surface with the assessor's eyes.

One control turned out to be a label rather than a control

Working through the SSH finding, we found the intrusion-prevention service running with no rules configured. It appeared in our own inventory as a protection while doing nothing at all, which is worse than not having it: an absent control gets noticed, a hollow one does not. It now has an active rule set with escalating bans.

This is the part of an assessment that does not appear in the findings list. Checking a reported item honestly means checking whether the mitigation you believed was in place is actually in place.

One finding we could not reproduce, and are not removing

The deprecated-TLS finding does not match the configuration or external probing: only TLS 1.3 is accepted, on every virtual host. The finding also contradicts itself, stating that TLS 1.2 is disabled while 1.0 and 1.1 are enabled — not a configuration anyone builds. We have asked the assessor to re-check it.

It stays on this page. A shorter list would look better and tell the reader less.

Conclusion

No findings from this assessment remain open. Five of the six are remediated and verified; the sixth could not be reproduced and is documented above rather than removed. One remediation is deliberately partial: SSH is not restricted by source address, for the reason stated under F-02, and we would rather name that trade-off than present a clean list.

Two changes went beyond the assessment, because reviewing its findings exposed them: an internal build identifier on a public endpoint, and an intrusion-prevention service that was configured with no rules.

This describes the state on 27 September 2026. It is a point in time, not a guarantee. A re-test to confirm the authentication changes independently is scheduled.

Why we publish this

A verification product that is opaque about its own security would undermine the thing it promises. Publishing an assessment is only meaningful if it includes what was found, not only what passed — so the open items are listed above alongside the remediated ones, with their status.

This assessment covered the demo environment. It is a point-in-time result: it describes what was true during the testing window, not a permanent guarantee. A re-test to confirm the remediation of F-01 and F-06 is scheduled.