Back to blog
  • security
  • api
  • cloud
  • appsec

How a missing API authorizer turned an SSO login into an open door

A single-page app was locked behind single sign-on. Its backend API had no authorizer at all, so we read records, enumerated an internal user directory, and more. Here is the full chain, anonymized.

Lunadra· Security and engineering10 min read
On this page

Most applications you use every day are two things wearing one name. There is the part you see: the single-page app in your browser, gated by a login. And there is the part that actually holds the data, a set of backend APIs the browser talks to. The login protects the first. Something else has to protect the second.

On a recent authorized engagement against a commercial real-estate platform, that second protection was missing. The web app sat behind single sign-on. Its APIs answered anyone who asked. Lunadra AI found the gap, walked through it, and kept walking: into a database of confidential deals, a bulk export of provider records, an unauthenticated write, and finally an internal user directory complete with names, roles, and clearance levels.

Here is the whole chain.

Before we start

This finding is remediated and fully anonymized. No target name, domain, account, or real data appears below. We keep the technique and drop everything that could identify who it happened to. The numbers are rounded for the same reason.

0Auth headers required
2Gateways exposed
12,000+Deals reachable
MinutesTo map the directory

Authenticating your front end is not authenticating your API

When a team puts an application behind single sign-on, it feels finished. You hit the site, you are bounced to the identity provider, you log in, you come back. The padlock is on the door.

But the browser app is not where the data lives. It is a courier. When it needs a record, it calls a backend API. Very often, on a cloud platform, that is an API gateway in front of a set of serverless functions. That gateway is a separate trust boundary, and it needs its own guard: an authorizer that checks the caller's token on every request before any function runs.

The failure mode is quiet and common. The front end gets wired to SSO early, everyone sees the login screen, and the backend gateway ships with no authorizer attached, because in testing the only thing ever calling it was the already-logged-in app. The app authenticates a person. The gateway authenticates nothing. Anyone who learns the gateway's address can skip the person entirely.

That is the whole bug. The rest is just walking through it.

What Lunadra AI found

AttackerWeb appBackend APIno authorizerRecords + identity

Two issues, the same root cause, two separate applications behind one platform:

Critical · 8.2   The data API had no authorizer. Unauthenticated callers could read individual deal records by id, pull bulk datasets of tens of thousands of records, and, worse, create records with no credential at all. Roughly 12,000+ confidential deals and 30,000+ provider records were reachable.

High · 7.5   A second gateway leaked the identity model. A single endpoint returned a complete user profile (name, role, granular permission codes, a confidentiality level, and the full list of markets a person could see) in response to nothing more than an email address. It also behaved as an enumeration oracle, answering differently for real accounts and invented ones.

Chained, the two turn into something larger than either: the first finding hands you employee email addresses, and the second turns each address into that employee's exact access and seniority. From outside the building, with no account, you can rebuild the org's authorization model mapped to named people. A spear-phisher's dream.

Why Lunadra AI looked there

A scanner sees a login page, fails to authenticate, and records "site requires authentication." It stops where the padlock is.

Lunadra AI starts from a hypothesis instead: the login I can see is not the thing holding the data. The interesting question is not "is the app protected" but "is the API the app talks to protected on its own, or does it just trust its caller?" That reframing, treating the front end and the API as separate boundaries and testing the second one directly, is what turned a locked door into a finding. Everything below is the agent confirming the hypothesis, one request at a time.

The chain, step by step

  1. 01
    Recon: the app handed over its own mapA public source map exposed the entire API surface

    The single-page app shipped a source map in production with original source content embedded. Source maps are a developer convenience (they let you debug minified code), but published openly they hand an attacker the app's internals.

    This one contained the templated environment configuration: the backend API gateway's address, and the names of every route behind it, from read endpoints to search, analytics, and record-creation endpoints. The reconnaissance phase was over before it started; the app had documented its own attack surface.

    source-map-scan
    $ scanning main.<hash>.js.map …
    ✓found route/api/<resource>
    ✓found route/api/<search>
    ✓found route/api/<analytics>
    ✓found route/api/<create-resource>
    ✓found route/auth
    20+ routes foundgateway <api-id>.execute-api.<region>.amazonaws.com
  2. 02
    Probe: call the API directlyNo token, no cookie. Just ask.

    With the gateway address in hand, the next question is one request: does the API check who is calling? Lunadra AI issued a plain request with no Authorization header, no session cookie, no signature, the kind of request the real app would never send.

    Request
    GET /api/<resource>?id=1 HTTP/1.1
    Host: <api-id>.execute-api.<region>.amazonaws.com
    Response · 200 OK
    HTTP/1.1 200 OK
    Content-Type: application/json
     
    {
      "id": 1,
      "client": "<redacted>",
      "notes": "<redacted>",
      "staffEmail": "<redacted>"
    }

    A 200, with a real record in the body. The gateway never asked for credentials. The hypothesis was confirmed: the API trusted that whoever reached it had already been vouched for. No one had.

  3. 03
    Read: enumerate records by idA sequential id is an invitation

    The record identifier was a small, sequential integer. When an unauthenticated read works and the key is predictable, reading one record means reading all of them: a classic insecure-direct-object-reference, at the scale of the whole database. Lunadra AI sampled a handful of ids across the range (minimal sampling, per the engagement's rules) and confirmed the same 200 and a full record each time. The reachable range covered 12,000+ deals, each carrying client identities, internal notes, and staff contact details.

  4. 04
    Escalate: bulk read, then unauthenticated writeAn empty body returned data, and created it

    The aggregation endpoints were even more generous. An empty POST {} to a search route returned multi-megabyte exports: tens of thousands of provider records in a single response, no filter, no auth.

    Then the same pattern against a record-creation route, expecting it to reject an empty body:

    Request
    POST /api/<create-resource> HTTP/1.1
    Host: <api-id>.execute-api.<region>.amazonaws.com
    Content-Type: application/json
     
    {}
    Response · 200 OK
    {
      "message": "successfully updated"
    }

    It wrote. An unauthenticated stranger could not just read the database, but add to it. (The few test records this created were flagged for the owner to delete during remediation.)

  5. 05
    Pivot: the same flaw, on the identity APIOne email returned a full user profile

    The deal records were full of staff email addresses. The source map also revealed a second gateway, a separate application for permissions and identity. Lunadra AI asked the obvious question: is this one protected? It sent a single unauthenticated request with one email address.

    Request
    POST /auth HTTP/1.1
    Host: <api-id-2>.execute-api.<region>.amazonaws.com
    Content-Type: application/json
     
    { "email": "<employee>@<redacted>" }
    Response · 200 OK
    {
      "data": {
        "id": "<user-id>",
        "name": "<redacted>",
        "active": true,
        "roleAssociations": [
          {
            "name": "<role>",
            "permissions": ["<permission-code>"],
            "tags": [
              {
                "tagType": "confidentiality_level",
                "data": { "level": "<n>" }
              }
            ]
          }
        ]
      }
    }

    A complete profile: the person's role, their exact permission codes, their confidentiality level, and, in the full response, the 100+ markets they were cleared to see. A non-existent email returned a different error, which makes the endpoint an enumeration oracle: feed it the directory of emails from the first finding, and it confirms which are real and returns each one's access. No rate limiting stood in the way. A short loop would map the entire workforce in minutes.

The decision path

The pivot from Finding 1 to Finding 2 is the part worth noticing. Reading 12,000+ deal records is a serious data exposure on its own. But Lunadra AI did not stop at "I can read data." It read the content of that data (staff emails), recognized them as inputs to a different system it had also seen in the source map, and tested whether the same class of flaw existed there. Data became identity. One unauthenticated API became a map of the organization's people and their clearance.

That is a small reasoning step, and it is exactly the step a signature-driven tool does not take. A scanner finds endpoints; it does not notice that the output of one is the key to another.

Root cause and the fix

The root cause was singular and architectural: the API gateways were deployed with no authorizer, so authentication lived only in the browser app. Because the same pattern appeared on two separate gateways, it was not a one-off mistake. It was a default that nobody had corrected at the platform level.

The remediation was correspondingly simple, and was applied to the whole stage rather than route by route:

  • Attach an authorizer to the entire gateway stage (a token or request authorizer that validates the SSO assertion) so every route rejects unauthenticated calls.
  • Stop publishing the source map (or strip embedded source content) so the API surface is not advertised.
  • Scope responses: a directory lookup should never return full permission arrays, confidentiality levels, or market lists, and never act as an existence oracle.
  • Add rate limiting and detection for sequential id reads and email enumeration, and review access logs for prior unauthenticated calls.

What this means for anyone shipping an API

None of these requests were clever. There was no exotic payload, no race condition, no memory corruption. Just plain HTTP asking plain questions. The finding was not a trick; it was an assumption, repeated across a platform:

  • SSO on the front end is not authentication on the API. They are two boundaries. The gateway needs its own guard.
  • A source map in production is reconnaissance you ship for the attacker. Treat your build output as part of your attack surface.
  • "It works in the app" hides "it works for anyone." If the only thing ever exercising an endpoint is the logged-in UI, the missing check is invisible until someone calls it directly.
  • Predictable ids plus missing auth equals the whole table. And read access to data is often a pivot to identity, not the end of the story.

The part a scanner cannot do

The bug was simple to exploit. It was not simple to find, not because the request was hard, but because finding it required a hypothesis a tool does not form: that the visible login is not the thing holding the data, and that the right move is to test the API as its own target, then chain what the data tells you into the next system. That is reasoning, not a wordlist.

That reasoning is what Lunadra AI runs, at scale, across an environment, thinking like an operator about how your own services trust each other, and proving the gaps with the evidence to fix them.

See what Lunadra AI finds in your environment

Have a system worth protecting?

Tell us what you are building. We partner with technical teams on security, architecture, and engineering.

Start a conversation