Gangnam Unni is a South Korean platform that connects people with cosmetic surgery clinics. On September 4, 2026, an API used to look up consultation records was accessed abnormally. The operator, Healing Paper, blocked that route — and then found the same attacker trying a different path the next day.
Confirmed total: 219,665 affected users (~160,000 South Korea, 48,000 Japan, 4,218 Taiwan, 1,591 Thailand, 481 mainland China, 5,308 elsewhere).
I'm not writing about the breach count. I'm writing about the entry point, because it's the one that keeps showing up and the one that's hardest to catch in review.
No exploit was involved
This wasn't a memory corruption bug, a stolen admin credential, or an unpatched CVE. Nothing was technically broken. An endpoint that was supposed to answer "what are this user's consultation records" answered for more records than it should have, to more callers than it should have.
That's broken object-level authorization — the API security category where the endpoint authenticates the caller but fails to verify that the caller is entitled to the specific object being requested. It survives code review because the function looks correct in isolation:
@app.get("/api/consultations/{record_id}")
def get_consultation(record_id: str, user: User = Depends(current_user)):
return db.consultations.find(record_id) # authenticated, not authorized
The reviewer sees current_user, sees that unauthenticated requests are rejected, and moves on. The missing check is the relationship between user and record_id, and it isn't visible in this function at all.
Why it's hard to catch
- It produces valid responses. Nothing throws. Status codes are 200. Error-rate alerting sees nothing unusual.
- It looks like normal traffic. Sequential or enumerable IDs are indistinguishable from legitimate use without behavioural baselining.
- Unit tests pass. You test that a user can fetch their record. Almost nobody writes the negative test for fetching someone else's.
- It's not in the dependency tree. No scanner will flag it. This is first-party logic.
The detail that should worry you most
The attacker came back through a second path the day after the first was closed.
That's the part I keep thinking about. Closing one route didn't end the incident, because the underlying authorization model was the problem — not the one endpoint that got caught. Whoever this was knew the system well enough to have a fallback ready, which means they'd mapped it.
If your incident response is "block the route that was abused," you're treating the symptom. The fix is fixing the authorization layer; otherwise you're playing whack-a-mole against someone who has already read your codebase's shape.
What to actually check in your own API
- Every object-scoped endpoint, list them. For each one, write down where the ownership check happens. If the answer is "in the service layer" or "the ORM scopes it," verify it — don't assume.
- Write the negative test. For each of those endpoints, add a test where user A requests user B's resource. It should fail. If that test doesn't exist, the guarantee doesn't either.
- Prefer non-enumerable identifiers. Sequential integers turn a single authorization bug into a full-table dump. UUIDs don't fix BOLA, but they raise the cost of exploitation dramatically.
- Rate-limit per identity, not per IP. Abnormal access to a lookup API is detectable — but only if you're counting per-authenticated-principal rather than per-source-address.
- Log object access with both IDs. Your audit log needs the requesting identity and the object accessed, or you cannot reconstruct scope after the fact.
The data made it worse
The exposed fields split into two layers. First, ordinary identity data: name, phone, email, date of birth, gender, residence, social account IDs, IP and device details.
Second, and this is why it's not a routine breach: procedures enquired about, clinic and doctor names, the stated reason for seeking treatment, clinical progress notes, uploaded consultation photographs, appointment times, and treatment and payment details.
A password is changed in a minute. A card is cancelled with a phone call. A pre-procedure photograph of someone's face, tied to their name and the thing they were self-conscious about, has no revocation mechanism. Korean security professionals quoted in local coverage warned about extortion using exactly that material.
That's the design lesson too: the reason this leak is catastrophic rather than merely bad is what was stored in a reachable place. Minimize the blast radius before you worry about the perimeter.
Originally published at CyberPicks.
Top comments (0)