SearchHealthcare Professionals Product Safety Partnering Global (EN) Patient Portal Client Portal Researcher Portal
Home / Security

Security

How the three portals are protected, what was wrong with them before, and the one thing these controls cannot do. The last part is the part that matters, so it is not at the bottom.

This page describes the public demonstration at baxtra.pages.dev. It does not describe the controls on internal systems that hold real clinical or laboratory data, and nothing here should be read as describing those.

Read this before anything else on this page. The portals are served as static files. There is no application server behind them, so every check described below runs in your own browser, on hardware you control. You can open the developer tools and edit the script that performs those checks. A determined person can therefore reach the portal interface without a valid password.

Client-side authentication is a user-experience control. It is not a security boundary, and it is not presented as one here. The reason that is acceptable is narrow and specific: there is no real patient, clinical, commercial or personal data behind any of these three logins. Every record in them is fabricated for demonstration. If real data were behind them, this design would be indefensible and we would not ship it.
What changed

Six defects found in the previous build, and what replaced them

These were real, they were live, and they are listed here rather than fixed quietly.

DefectWhy it matteredWhat it is now
The password was in the source
Two scripts contained the account name and password as plain text.
Anybody who opened the file could read the credentials. No attack was required, only curiosity. Passwords are stored only as PBKDF2-HMAC-SHA256 digests: 600,000 iterations, a distinct 16-byte salt per account. The password appears in no file served by this site.
A query string was accepted as proof of sign-in
Adding ?s=researcher to a portal address produced an authenticated session.
This was a complete authentication bypass. It needed no password, no tooling and no knowledge beyond the parameter name. Removed from both scripts. The only thing that can produce a session is a successful two-factor sign-in, and the role is never read from the address bar.
The failure message printed the password
A failed sign-in replied with the correct credentials.
An attacker learned the password by guessing wrongly once. One generic message. It does not say which factor was wrong, and it does not reveal whether the account exists. A derivation runs even for unknown accounts so the response time does not answer that question either.
No second factor A single guessed or reused password was sufficient. A time-based one-time code is required on every sign-in: RFC 6238, HMAC-SHA1, six digits, 30-second step, one step of clock skew tolerated. Secrets are 160-bit random values held only as ciphertext and enrolled to the account holder’s own authenticator application.
Unlimited guessing Nothing counted or slowed repeated attempts, so a four-digit password fell in seconds. Attempts are counted. Backoff doubles from the second failure and a fifteen-minute lockout applies at five. The 600,000-iteration derivation also makes each attempt deliberately slow.
Sessions never ended and were forgeable
The session was unsigned JSON in browser storage with no expiry.
A session lasted indefinitely, and its contents — including the role — could be edited by hand to escalate privilege. Eight-hour absolute lifetime, fifteen-minute idle lock, an HMAC-SHA256 integrity tag over the payload, and binding to a single role. A session that fails any of these is discarded and the event is logged.
Controls now in place

What protects a portal session

Key derivation

PBKDF2-HMAC-SHA256, 600,000 iterations, 16-byte per-account salt, 256-bit output, through the browser’s Web Crypto implementation. The iteration count is set so that a single attempt is perceptibly slow, which is the point.

Second factor

Required every time, not only on a new device. Codes are verified against the current step and one either side, so a slow clock does not lock a user out while the acceptance window stays at ninety seconds.

The secret is sealed, not stored

A single password derivation produces 64 bytes: the first half is the digest kept in the file, the second half is an AES-256-GCM key written down nowhere. The authenticator secret exists only as ciphertext, so it cannot be read out of the source the way the old password could. Enrolment unseals it in the browser at /authenticator and requires the password.

Constant-time comparison

Digests and codes are compared without an early exit, so a comparison cannot be timed character by character. Against a local attacker this buys nothing, but it is correct and it costs nothing.

Session integrity and binding

Each session carries an HMAC-SHA256 tag over its payload and names exactly one role. A session issued for the client portal is refused by the researcher portal, and an account without the role is refused at sign-in.

Expiry and idle lock

Eight hours absolute. Fifteen minutes of inactivity locks the screen behind the password, with a warning two minutes before. Reaching the absolute limit ends the session outright rather than locking it.

The shell never renders early

The portal interface is held behind an opaque curtain until the session has actually verified. A failed check redirects without ever having painted the workspace.

Clean sign-out

Signing out removes the session and deletes anything the workspace cached in that browser, so the next person at the same machine finds nothing.

Authentication log

Sign-ins, failures, lockouts, locks, unlocks, integrity failures and role mismatches are recorded and shown inside each portal under Security. It is local to the browser and is not transmitted.

No framing

X-Frame-Options: DENY and a frame-ancestors 'none' policy on the portals, with a script-level check as a fallback if a header is ever dropped.

Transport and browser policy

Headers served with every response

HeaderValue and purpose
Content-Security-PolicyEverything restricted to same-origin. No third-party script, style, font, frame or connection is permitted; object-src and frame-src are 'none', base-uri is 'none' and form-action is restricted to this origin.
Strict-Transport-SecurityOne year, including subdomains. The site is not reachable over plain HTTP, which also means the Web Crypto interfaces the sign-in depends on are always available.
X-Content-Type-Optionsnosniff, so a response is never reinterpreted as a type it did not declare.
X-Frame-OptionsSAMEORIGIN across the site and DENY on the portals and login pages.
Referrer-PolicyStrict origin cross-origin site-wide, and no-referrer on the portals so a portal address never leaks outward.
Permissions-PolicyGeolocation, microphone, camera, USB and payment all denied. This site has no use for any of them.
Cache-Controlno-store on portals and login pages, so a signed-in page is not left in a shared browser cache.
X-Robots-Tagnoindex, nofollow, noarchive, nosnippet on the portals, matching the exclusions in robots.txt.

One honest caveat on the policy above: it permits inline script and inline style, because the existing pages use both. That materially weakens what a content security policy can do about script injection. Removing it means moving every inline handler into a file, which is planned and not yet done. It is listed as outstanding below rather than described as complete.

The limits, stated properly

What none of this achieves

Every control on this page executes in the visitor’s browser. The visitor owns that runtime. They can set a breakpoint, edit the script, replace the verification function with one that returns true, or simply write a session object into storage. No amount of care in the code changes that, because the code is the thing being asked to police itself.

The session integrity tag is a specific example worth spelling out. It is a real HMAC and it really does reject a hand-edited session. But the key it uses is derived from account material that is also in the script that checks it. So it defeats accidental corruption and casual tampering, and it does not defeat anybody who has read the file. Describing it as tamper-proof would be a lie, so we are describing it as tamper-evident against the careless, which is what it is.

One thing the sealing does genuinely achieve, and it is worth being precise about the difference. The old design put both the password and the authenticator secret in a file anybody could read, so the second factor was decoration. Sealing the secret under the password means an attacker who has only read the source has neither factor, and codes cannot be generated at all. It does not help against somebody who already has the password, and it does not stop them editing the verification out of the script — but it removes the case where reading one file was enough to sign in as the account holder.

The lockout counter and the authentication log live in browser storage for the same reason, and can be cleared from the browser. They slow a careless attempt and give the account holder a record. They do not stop a determined one.

The only control that would actually stop an attacker is enforcement in front of the origin, so that an unauthenticated request never receives the file at all. On this hosting that means an identity-aware proxy sitting ahead of the /portal-* paths, checking identity before a byte is served. That is the correct fix. It is not yet configured, and it is the first item in the table below rather than something omitted to make this page look better.

Outstanding

Known gaps, with what each one needs

GapEffectWhat it needsTarget
No server-side enforcement in front of the portal paths. The portal interface can be reached by editing client-side script. This is the significant one. An identity-aware proxy in front of /portal-* with a policy allowing named accounts only.Q4 2026
The content security policy permits inline script and style. Weakens the policy’s value against script injection. Moving inline handlers and styles into files, then removing 'unsafe-inline'.Q4 2026
No subresource integrity on internal scripts. A compromise of the hosting account could serve altered scripts undetected. Build-time digests emitted into every script tag.Q1 2027
The one-time code is displayed on the login page. The second factor provides no protection. Closed, 1 September 2026. Nothing is displayed. Secrets were replaced with 160-bit random values, sealed under the password, and enrolled to a real authenticator application.Done
The lockout counter is client-side. Can be cleared by the person being rate-limited. Rate limiting at the edge, keyed on address rather than browser.Q4 2026
No password rotation, history or complexity policy. A weak demonstration password stays weak. Moot once identity is handled by a real provider; not worth building client-side.With the proxy
Reporting

If you find something

Write to security@baxtra.pages.dev. There is a security.txt at the conventional location with the same detail. We acknowledge within five working days, and we will credit you if you would like to be credited. We do not run a paid bounty.

Reports that the portals’ client-side authentication can be bypassed by editing the page are already known, are documented above, and are accepted rather than fixed — so please do not spend your time on them. What we would genuinely like to hear about: anything affecting a visitor who has not tampered with their own browser, a header or policy misconfiguration, material that should have been withheld under the disclosure policy but was not, and anything on this site that misstates the regulatory status of an investigational product.

Please do not run automated scanning that degrades the service for other people, and please do not attempt to reach any system that is not baxtra.pages.dev. Nothing else in our estate is in scope for testing.

Related

The same principle applies to what we publish

The disclosure log states how much was withheld from every page on this site rather than leaving it implied, and the accessibility statement lists every known defect with a date. Naming the gap is more useful than claiming there isn’t one.

Disclosure log Accessibility