Home/Journal/Teardown
TeardownCriticalserverlessauthenticationapi-gateway

The Billing API With No Front Door

Serverless billing functions sat on the internet with no auth, returning live patient billing records. Here is how we found it and how to close it.

Rahul Dharan··8 min read·a cardiac-telehealth clinicMissing authentication (CWE-306)

The setup

The target was a cardiac-telehealth clinic - a company delivering heart care remotely, which means it holds clinical data, patient billing, and the machinery that connects the two. The visible product was a polished single-page app that opened with a Google sign-in. You could not see anything without authenticating, and the front-end enforced that cleanly.

A clean front door is reassuring, and it is also exactly the thing worth testing. A single-page app is a client. It talks to a backend, and that backend is usually a set of serverless functions, each with its own internet-reachable URL. The login the user sees governs the app. It does not, on its own, govern the functions. So the question we cared about was not “does the app require login” - it plainly did - but “does the backend.”

Recon

Working black-box, from the open internet with no valid account, we studied the API surface the front-end used. A single-page app is transparent about who it talks to: the code that runs in the browser names the backend functions it calls. From the client we enumerated the serverless functions behind the product - a billing export, an outbound-fax action, a referral-intake handler, and others - each a distinct function with its own endpoint.

The front-end attached a Google sign-in and sent an authorization header on its requests. The interesting question was whether the backend actually required that header, or whether it simply received one because the app happened to send it.

The hypothesis

The pattern we were testing for is one of the most common in serverless products: the front-end enforces sign-in, and the team reasons that because the only client is the authenticated app, the functions behind it are protected by extension. The functions themselves are left with their gateway authorization set to none, sitting directly on the internet, trusting a gate that lives in the browser.

So the hypothesis was direct: at least one of these functions would answer an unauthenticated request. And because the functions handled billing and clinical intake, a function that answered without auth would return real patient data, not a harmless status page.

What we tried

We called the billing-export function directly, with no session and no valid token - the same request the app makes, stripped of authentication.

It returned live patient billing records.

Not an error, not an empty set, not a redirect to login. Real billing PHI, paginated, ready to be walked page by page. The function was reachable from anywhere and asked for nothing.

Then we stopped. We read a single page to establish that the data was real and to understand its shape, and we did not paginate through the customer base. We also confirmed - by inspecting the other functions the same way - that a separate endpoint would send an outbound fax as the clinic, and that a referral-intake endpoint accepted anonymous submissions. We confirmed those were reachable and unauthenticated. We did not send a fax, and we did not submit referral data. Proving the door opens does not require walking through it.

What we found

The billing-export function returned live patient billing records to an anonymous caller:

-> GET (unauthenticated call to the billing-export function, page 1)
   Authorization: none

<- {
     "records": [
       { "patientName": "...", "amount": ..., "service": "...", "invoice": "..." },
       ... live patient billing records, paginated ...
     ],
     "nextPage": "<cursor>"
   }

The nextPage cursor is the detail that turns one leaked page into the whole book. The function was built to paginate, so an unauthenticated caller could follow the cursor from the first record to the last. There were thousands of records behind it.

Two sibling functions on the same surface compounded it. One let anyone send an outbound fax as the clinic - a channel that, in a healthcare setting, carries real weight and real abuse potential. Another accepted anonymous referral submissions, letting anyone inject records into the intake pipeline. All three shared the same root: the function sat on the internet and did not authenticate the caller.

Notably, the deeper clinical data was locked. This was not a company that failed to think about security. The gap was specific and structural: a set of serverless functions whose gateway routes were configured with authorization set to none, trusting the front-end’s Google sign-in as the only gate.

Why it happens

No one decided to expose patient billing. This is the ordinary shape of shipping a serverless product fast.

In a serverless architecture, every function is its own internet-reachable service, and authentication is a per-route setting that has to be applied deliberately to each one. The default in many gateway configurations is permissive: a route with no authorizer specified is open. When a team’s mental model is “the app requires login, so the backend is protected,” that per-route setting reads as redundant, and it gets left at none. The front-end’s sign-in is doing visible work - it stops users at the UI - so it feels like the security boundary. It is not. It governs the client. The functions govern themselves, and each one that skipped its authorizer is a front door with no lock. Every instance of this is velocity, not carelessness.

For developers

If you run serverless functions behind a single-page app, four controls close this:

  1. Authenticate every function at the gateway. Set each route’s authorization to a real authorizer - a JWT validator, a Cognito authorizer, a custom one - never none. The front-end sending a token is not the same as the backend checking it.
  2. Validate the token inside the function too. Defense in depth: even with a gateway authorizer, the function should verify the caller’s identity and scope before it returns data. A gateway misconfiguration should not be the only thing standing between the internet and patient records.
  3. Deny by default. Configure the platform so a route with no explicit auth is unreachable, not open. The safe default is that a new function is closed until someone deliberately opens it - the opposite of the common posture.
  4. Treat the front-end’s login as a UX gate, not a security boundary. It controls what the app shows. It controls nothing about what the backend returns to a direct caller. Assume every function will be called without your app in front of it, because it can be.

And one detection step: enumerate your own functions and call each one from a clean client with no token. Anything that returns data instead of a 401 is a front door with no lock.

The takeaway

A single-page app’s sign-in screen protects the app, not the API behind it. Every serverless function is its own internet-reachable service, and each one has to authenticate the caller itself. When authorization is left at none because the front-end “already checks login,” the backend has no front door at all - and the most direct request in the world returns live patient data.

This teardown is one instance of a pattern we see repeatedly. We wrote up the general case, and how to defend against it, in The Sibling Service That Skipped Authentication.

Frequently asked

What does missing authentication on a serverless function mean?
It means a cloud function - an AWS Lambda or similar - is reachable directly over the internet through an API gateway whose route authorization is set to none. Any request to that URL runs the function, so anyone who finds the endpoint gets whatever the function returns, with no login required.
Does front-end login protect a backend API?
No. A sign-in screen in the single-page app only controls what the app's own UI shows a user. The backend functions the app calls are separate internet-reachable services. If those functions do not authenticate the request themselves, an attacker can call them directly and never touch the front-end at all.
How do you secure serverless billing functions?
Authenticate every function at the API gateway - set route authorization to a real authorizer, not none - and validate the caller's token inside the function. Deny by default, so a route with no explicit auth configured is unreachable rather than open. Never rely on the front-end's login as the only gate.

This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.

How it works →