← Blog

12 min read

Anyone, Please Interact: Two APIs That Never Asked Who I Was

Two penetration tests, two APIs that never checked. One handed HIPAA-protected medical records to the open internet. The other let anyone pull live card data from an app the bank swore was still in development. Same bug, two costumes.

Anyone, Please Interact: two wide-open APIs, medical records and live card data

Every web application is really two applications.

There is the one you see: the login screen, the role dropdown, the delete button greyed out because you are not an administrator. That one is theater. It is staged for the browser, and it is very good at making you feel supervised.

Underneath it is an API doing the actual work, answering questions all day. The only question that matters is whether it ever bothers to ask who is doing the asking.

Twice, on two different engagements, I found APIs that did not ask. One was holding medical records. The other was holding live card numbers. Neither one so much as glanced up.

I first wrote this up for CLA in 2021, and the founder-hat version lives at Show Up Show Out Security. This is the version where I tell you what I was actually thinking while it was happening.

The distinction everyone nods at and then skips

An API is how two systems talk to each other. Your front end asks, the API answers, and somewhere in the middle is supposed to be a moment where the API decides whether you deserve an answer.

That moment is two separate checks, and conflating them is where most of this goes wrong.

Authentication is who are you. Authorization is what are you allowed to touch. Plenty of applications nail the first one and treat the second as a stretch goal, which is roughly like checking IDs at the door of a bank and then letting everyone wander behind the teller counter because, well, they got in.

Authorization also has to happen on every request, not just the one where you logged in. A bearer token that gets validated at sign-in and then trusted forever is not access control. It is a wristband.

OWASP has ranked exactly this as the number one API risk since the list existed. It was API1 in 2019 as Broken Object Level Authorization, and when they refreshed the list in 2023, it was still API1. Six years at the top of the charts. Some bugs have longevity that most bands would envy.

Exhibit A: the medical records that answered to absolutely anyone

Government-sector organization, handling sensitive medical and personal information. The web application was internet-facing, meaning anyone in the world with a browser could reach it.

During the engagement we found multiple endpoints where authentication was not required at all. Not weak authentication. Not guessable tokens. Not required. If you knew the URL and the shape of the request, the API answered you like an old friend.

In plain terms: anyone on the internet with the right prerequisite knowledge could pull HIPAA-protected data straight out of that application. And “prerequisite knowledge” is doing far less work in that sentence than it sounds like, because the way you obtain it is opening dev tools and watching the requests the application’s own front end is already making. That tooling is free, built in, and shipped on every computer on Earth.

Step one, ask what documents exist. This is the request that enumerates a member’s documents.

A raw HTTP GET to /submit/get-docs.htm with requestId=16912 and empty username and role parameters, carrying no authorization header and no cookie

Look at what is not in that request: no authorization header, no cookie, nothing that identifies me as anyone at all. Then look at what is in it. The application passes identity as username= and role= query parameters, and I sent both of them empty.

That is the whole finding in one line. The API was handed a blank identity and a record ID, and it decided the record ID was the more important of the two.

Step two, receive the filing cabinet. It answered with 200 OK and a JSON list of every document on the account.

An HTTP 200 JSON response listing document metadata including rmRequestDocType and a redacted rmRequestDocName ending in "Medication list.pdf"

Document names in a medical system are not metadata in any meaningful sense. “Medication list” tells you something about a person before you have opened a single file.

Step three, ask for a specific document. Same pattern, one ID deeper, still nobody checking.

A raw HTTP GET to /submit/get-doc-by-id.htm with requestId=11366 and docNum=6, again with empty username and role and no authorization header

Step four, get handed a PDF. Content-Type: application/pdf, Content-Disposition: attachment, 681,471 bytes of it.

An HTTP 200 response with Content-Type application/pdf and a Content-Disposition attachment header carrying a redacted patient filename

And this is what was inside.

A redacted long term care screening report showing labelled fields for applicant name, SSN with verified status, address, phone numbers, birth date, Medicaid number, race, ethnicity, and admission notes

A long term care screening report. Name, verified SSN, home address, phone numbers, date of birth, Medicaid number, race, ethnicity, and narrative notes about when this person was admitted and where they lived before that.

Everything blurred in that screenshot was perfectly legible to me, and would have been perfectly legible to anyone else who sent that request. There was no exploit in this chain. There was no payload. There were four ordinary GET requests with a number incremented in them.

Then it got worse, in the way findings do when you keep pulling the thread.

The API did not only read. It also handled document upload and deletion, with the same absent authorization.

A raw HTTP DELETE to /submit/delete-rm-doc.htm with requestId=16723 and docNum=4, no authentication headers and no username supplied

Note the verb. DELETE, no username, no authentication headers, and the server’s considered response was this:

An HTTP 200 text/plain response with a Content-Length of 1 and a body consisting of the single character 1, indicating successful deletion

Content-Length: 1. The body is the character 1. Somewhere in that codebase, 1 means “yes, that patient’s medical record is gone now.”

So the same gap that let me enumerate and download medical records let me delete them. Every document, on every account. That is not a data breach so much as a data breach with an availability event stapled to it and a HIPAA garnish on top.

We worked with the client, handed over the details, and they remediated it. That is the boring, correct ending, and it is the one I want every time.

The other ending has a price list. HIPAA civil penalties are tiered and inflation-adjusted every year, and the annual cap for the worst tier now runs past two million dollars per violation category. That is before breach notification costs, before outside counsel, and before the letter that has to go out to every single patient explaining what happened to their records.

Exhibit B: the card app that was “not in production yet”

Different engagement. Internal penetration test, financial institution. The application managed debit and credit cards the institution had issued, and it supported the administrative work: look up card details, order and issue a new card, block a card, raise point of sale limits.

Read that feature list again. That is not a reporting tool. That is the console. And here is how it greeted you.

The card application dashboard offering six tiled options: Search for a Card, Order a Debit Card, Reissue a Card, Block a Card, Card Maintenance, and Update POS Limits

Six friendly tiles. Order a card, reissue a card, block a card, change someone’s point of sale limits. Every one of those is a real action against a real person’s money, rendered with the visual confidence of a hotel check-in kiosk.

I used the graphical interface exactly as intended. Clicked through the workflows like a well-behaved employee, and watched what the front end sent.

The application's card search results listing cardholder names, card numbers, expiry dates, issue numbers, and card status, all redacted

That is genuinely all “reverse engineering the API” means most days: you let the application show you its own homework. Search for a card, watch the request go out, read the response, and now you know the shape of the endpoint and the shape of the object it returns.

Which left one question worth asking. What happens if I send that request without being anybody?

A Burp Suite request and response pair showing a GET to /cardstatus/api/Member/id/Cards with no authorization header, returning a 200 with full card records including cardholder name, address, card type, card number, expiry, and account ID

GET /cardstatus/api/Member/{id}/Cards. No authorization header. HTTP/1.1 200 OK, and 6,206 bytes of somebody’s financial life: cardholder name, street address, city, state, ZIP, embossed name, card type, card number, expiration date, issue date, activation date, status Active, and the internal account and party identifiers.

The member ID sits right there in the URL path. Change it, get somebody else. That is not a subtle flaw buried in a session handler. The object identifier is the first thing you see in the request, and the API treated it as both the question and the permission to answer it.

No authorization checks were performed. Anyone with access to the application could extract full account and card details for any customer. Not their own customers. Any customer. All of them, if they were willing to wait for the loop to finish.

Then came the preliminary observations meeting, which is my favorite part of this job because I get to watch a room process something in real time.

The organization’s response was that the application was still in development and had not gone to production. Which would have been an enormous relief, right up until the follow-up answer: the data in it was live production data.

So the finding count went from one to two.

PCI DSS does not allow live PANs to be used for testing or development. When I originally wrote this in 2021, that was control 6.4.3 under v3.2.1. In PCI DSS v4.0 the requirement was renumbered to 6.5.5, and reworded to say that live PANs are not used in pre-production environments unless that environment is part of the CDE and protected to every applicable PCI DSS requirement. v3.2.1 retired in March 2024, so if you are still citing 6.4.3 for this one, you are now citing the payment page script requirement instead, and your QSA is going to catch that before you do.

The rule survived the rewrite. It just moved offices. “It is only dev” has never once been a compensating control.

Both of these are the same bug wearing different clothes

The finding in the medical application and the finding in the card application look nothing alike in a report. Different sectors, different regulators, different threat models, different fine schedules.

They are the same bug.

In both cases the application enforced the rules and the API did not. The UI knew perfectly well which records you were allowed to see. It rendered your account and not someone else’s, it greyed out the buttons your role could not press, and it did all of that faithfully, in your browser, on your machine, where I get to edit it.

Nobody is obligated to use your front end. It is a convenience for humans, not a security boundary. Any check you implement only in JavaScript is not a control, it is a strongly worded suggestion. And the greyed-out delete button is not a permission model. It is a mood.

What has changed since 2021, and what stubbornly has not

What has not changed: broken object level authorization is still the number one API risk, in the same top slot, for the same reason.

What has changed is the discovery half of the equation, and it changed a lot.

In 2021, an unauthenticated endpoint sat quietly waiting for a curious person to wander past. Today it gets found by continuous internet-wide scanning, by API-aware scanners that fuzz object IDs by default, by anything that ingests the OpenAPI spec you helpfully published, and increasingly by automated agents that will enumerate an endpoint several million times because nobody explained to them why they shouldn’t. Time to discovery for an exposed endpoint is no longer “eventually.” It is closer to “this week.”

The other change is volume. Every mobile app, every single-page app, every partner integration, every webhook added endpoints. Then teams shipped v2 and left v1 running because something might still be calling it. Shadow APIs, zombie APIs, that one internal service someone stood up for a demo in 2022 and never turned off.

Which makes inventory a security problem rather than a documentation problem. You cannot authorize requests to an endpoint you have forgotten you shipped.

The fix, which is not exotic

Authorize every request, against every object. Not at login. Not at the session layer. On the request, for the specific record being touched, every time. The question is never “is this user authenticated,” it is “does this user have a claim to this record for this action.”

Enforce it server side, as close to the data as you can get. Ideally in a place a developer cannot forget to call, because eventually one will. A shared policy layer that every handler goes through beats a hand-written check copied into forty-one controllers, where all it takes is the forty-second.

Never trust an identifier that arrived from the client. Account IDs, document IDs, card IDs, and customer IDs in a request body are inputs, not credentials. Treat a UUID as unguessable if you like, but do not treat it as authorization. Obscurity is a speed bump, and the tooling drives a truck.

Keep live data out of pre-production. Not because a standard says so, though one does, but because dev environments get looser monitoring, weaker access control, and a much longer list of people with a login.

Inventory your endpoints, then test the endpoints. A scanner that clicks through your UI only sees the paths your UI chose to take, which means it tests the exact subset of your API that already behaves. Point the testing at the API surface directly, including the endpoints your front end stopped calling two releases ago.

The front door thing

Not securing your APIs with authentication and authorization is the equivalent of leaving your front door wide open. I wrote that line in 2021 and I have not found a reason to soften it since.

I will extend it, though. It is leaving the front door open in a building where everyone assumes the lock is somebody else’s responsibility.

Because almost none of the organizations I test are negligent. They are busy. Somebody shipped a fast endpoint to unblock a release. A vendor wired up an integration with the auth model they use everywhere else. A developer moved on before the second half of the checklist got written. The gap is hardly ever created by someone who did not care. It is created by everyone assuming that someone else had already checked.

So go check. Open dev tools on your own application, watch a request go out, and then replay it with a different object ID. Then replay it with no token at all. That is ten minutes of your afternoon, and it is a better-spent ten minutes than most of what is on your calendar.

If either of those comes back with someone else’s data, congratulations. You found it before somebody with worse intentions and better tooling did.

Further reading

If you want to talk about application testing or getting eyes on your API surface, reach out.