Home/Journal/Teardown
TeardownCriticalauthenticationauthorizationapi-security

The Maintenance API That Ran the Network

An EV-charging operator shipped a maintenance API with its auth commented out and reachable from the internet. Here is what it exposed and the fix.

Rahul Dharan··9 min read·an EV-charging networkMissing authentication on an admin/maintenance interface (CWE-306)

The setup

The target was a large EV-charging network operator - the kind of company that runs physical charging hardware in the field, a wallet and payments layer for drivers, and an operator backend that ties the two together. That is a broad and physical attack surface: real chargers on real streets, money moving through driver wallets, and a control plane that manages tenants, users, and the hardware itself.

We focused on the operator backend. In a network like this, the backend is where the leverage is: it is the software that speaks to the chargers, holds the tenant and user records, and runs the financial operations. A flaw in the public product is contained. A flaw in the backend can reach the whole fleet.

Recon

Working black-box, from the open internet with no account, we mapped the operator backend and enumerated the routes it answered. Most of the application behaved correctly - the main API enforced authentication, rejecting unauthenticated calls as it should.

Then the surface got interesting. Alongside the properly gated API, there was an internal maintenance and utility API - dozens of routes with names that read like operations tooling rather than product features. Housekeeping, administrative actions, internal jobs. Routers like that are supposed to live behind the same auth as everything else, or off the internet entirely. This one answered.

The hypothesis

Applications built by fast-moving teams often carry a second tier of routes that the main auth story forgets: internal maintenance and utility routers. They get built for operators, tested from inside, and mounted alongside the product API. The assumption baked into them is “nothing external reaches this.”

So the hypothesis was that this maintenance router had been mounted without the authentication the rest of the app enforced - either never wired to the global guard, or with its per-route auth switched off during development and never switched back. If that was true, dozens of operational actions would be callable by anyone who found the routes, with none of the checks the main API applied.

Given what the routes were named, “operational actions” meant financial, tenant, and user operations - the destructive kind.

What we tried

We called the maintenance routes without any credentials.

They answered. The per-route authentication had been commented out, and the router was reachable from the internet, so the checks that guarded the rest of the application simply were not there. The routes exposed destructive financial, tenant, and user operations - the operational control plane of the network, open.

To prove reach without causing harm, we invoked a single benign action: a live administrative send-email operation. It executed unauthenticated, which confirmed the routes were not just listed but functional against the real backend.

-> POST <maintenance/util route>   (no Authorization header)
<- 200 OK   (administrative action executed against the live backend)

What we deliberately did not do defines the engagement. The destructive writes - the financial, tenant, and user operations that could have altered or destroyed real records - we did not invoke. Per the rules of engagement, proving the door was open was the objective; walking through it and breaking things was explicitly out of scope. We confirmed the routes existed, were reachable, and were callable, and we stopped there.

Two adjacent findings compounded the picture. First, the charger-control layer spoke an EV-charging protocol that did not authenticate the device, which allowed unauthenticated charger impersonation across hundreds of chargers - a client could present itself as hardware it did not own. Second, an unauthenticated biometric access-control interface allowed read of a physical-access system, reaching from the software estate into the physical one.

What we found

The core finding was an internal maintenance and utility API, dozens of routes deep, with its per-route authentication commented out and the whole router reachable from the internet.

Main product API:      auth enforced      -> 401 without credentials
Maintenance/util API:  auth commented out -> 200 without credentials
                       (financial, tenant, user operations exposed)

The main API did the right thing. The maintenance router, mounted beside it, did not - not because of a subtle bypass, but because the auth was literally switched off in the code and the router was left exposed. Everything gated on the product side was ungated one router over.

The compounding findings widened the blast radius from the backend into the field:

Charger protocol:  device identity unauthenticated
                   -> charger impersonation across hundreds of chargers

Biometric access:  interface unauthenticated
                   -> read of a physical access-control system

Together these reached across the whole stack: the operator backend, the charging hardware, and a physical-access system - each missing an authentication check that the rest of the platform enforced.

Why it happens

No one decided to publish an unauthenticated admin API. This is what a “temporary” development shortcut looks like when it ships.

A maintenance router gets built for internal operators. During development, the auth guard on it is inconvenient - it slows down testing every housekeeping route - so someone comments it out to move faster, with every intention of restoring it. Then the build ships. The commented-out auth is invisible in normal use because the routes are not part of the product, nobody hits them by accident, and the team assumes the router is unreachable from outside. The assumption of unreachability is doing all the security work, and it is wrong the moment the host is internet-facing.

The device-protocol and biometric findings share the same root: an interface built for a trusted context - a charger talking to its backend, a badge reader on a wall - mounted without authentication because the context was assumed to be closed. Each is a control plane that was never expected to face an untrusted caller, and each faced one.

For developers

If you run an operator backend or any application with internal tooling, four controls close this:

  1. Apply the global auth guard to every router, including maintenance and utility. Internal routes are not exempt from authentication - they are the routes that most need it, because they wrap the most powerful actions. There should be no path by which a router mounts without inheriting the same guard the product API uses.
  2. Never comment out auth in a shared build. A disabled check that helps local testing becomes a live vulnerability the moment it ships. If you must relax auth to develop, do it with a local-only flag that cannot survive into a deployed build - not by editing the guard out of the code.
  3. Segment internal control planes off the public internet. Maintenance and utility APIs that serve operators have no reason to be internet-reachable. Bind them to the internal network, put them behind a VPN or mesh, and alert on any public exposure as a misconfiguration.
  4. Authenticate device protocols. Hardware that talks to your backend must prove it is the hardware it claims to be. An unauthenticated device protocol lets anyone impersonate a charger, a reader, or a sensor across the fleet. The same applies to physical-access interfaces - a badge system is a control plane too.

And one detection step: audit every mounted router for the auth guard, and grep your codebase for commented-out authentication before every release. A single router with its check disabled is the whole exposure.

The takeaway

The routes you assume nobody reaches are the ones an attacker reaches first, and a maintenance router with its auth commented out is a full control plane published to the internet by accident. Authentication is not something the main API does on behalf of the whole application - every router carries its own, or it carries none. The same discipline extends past the software: a charger protocol and a badge reader are control planes, and each needs to prove who is calling before it acts.

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 is a missing authentication vulnerability on a maintenance API?
It is an internal administrative or utility interface that reaches the internet without the authentication the rest of the application enforces. Because these routers wrap powerful operational actions, an unauthenticated one hands a stranger the same control an internal operator has.
Why do internal maintenance routers end up unauthenticated?
They are often built for a trusted internal context, so the auth guard gets commented out during development to make testing quick. If that router ships that way and the host is reachable, the temporary gap becomes a permanent, internet-facing hole.
How do you secure an internal admin or maintenance API?
Apply the global authentication guard to every router including maintenance and utility routes, never comment out auth in a shared build, segment internal control planes off the public internet, and authenticate device protocols so hardware cannot be impersonated.

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

How it works →