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.
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.
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.
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.
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.
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.
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.
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.
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.
Thirty-eight controls were independently confirmed to work. A selection:
role and isCustomerAdmin fields are silently discarded; the project model has no role field to elevate. Verb tampering on read-only endpoints is rejected.?scope=all as a non-admin is ignored and scope stays restricted to the user's own tickets.security.txt is published with contact details, a PGP key reference and an expiry date.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.
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.
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.
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.
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.
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.
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.