
The Public Asset Trap: Why 'Public' Should Still Mean 'Authorized'
DevGuard's latest vulnerability (GHSA-6p54-fw2f-q7qf) exposes a dangerous pattern in multi-tenant apps: failing to enforce authorization on 'public' resources. Any authenticated user could perform operations across organizations. Here's how it happens, how to test for it, and how to fix it permanently.
Yesterday, GitHub published advisory GHSA-6p54-fw2f-q7qf for DevGuard, a vulnerability management platform. The summary was stark: on any DevGuard instance with public assets, any authenticated user — including someone from an entirely different organization with no membership or role — could perform actions they shouldn't. The root cause? Improper authorization on those public assets.
The details are still thin, but the shape of this issue is painfully familiar to anyone who's pentested multi-tenant applications. It's a class of bug I've seen across platforms, from CI/CD tools to project management software: a developer marks something "public," and suddenly the entire authorization stack falls away. I call this the public asset trap, and it's worth dissecting to understand why it happens, how to find it, and how to fix it permanently.
What Actually Goes Wrong?
Here's the mental model most developers carry: if a resource is marked "public," it should be accessible to anyone. And "anyone" often gets translated in code as "any authenticated user" or even "any visitor." So the application checks the public flag, and if it's true, the request sails through without any further authorization.
That's fine if the only thing you can do with a public asset is read it. But in DevGuard, and in countless other platforms, that public flag determines the visibility label, not the operation permissions. The bug emerges when the application treats "public" as a blanket exemption from all access controls.
Consider a scenario: Organization A has a project with a vulnerability report marked public. Organization B's user, who has no membership in Organization A, logs in and navigates to that report. They should be able to see it — it's public. But what if they can also edit it? Or delete it? Or comment on it? That's where the authorization failure lives. The advisory notes that any authenticated user could perform something (the details are truncated, but likely modification) on public assets belonging to another org. That crosses a critical boundary.
The impact isn't just data leakage. It's integrity compromise and, in some systems, privilege escalation. If an attacker can modify a public asset that other users rely on, they can inject malicious content, mask vulnerabilities, or pivot attacks across tenants.
How the Code Falls Into the Trap
I've reviewed enough codebases to recognize the pattern instantly. It looks something like this in a typical Node.js/Express middleware:
// Vulnerable middleware - simplified
async function requireAssetAccess(req, res, next) {
const asset = await db.findAsset(req.params.assetId);
if (!asset) return res.status(404).json({ error: 'Not found' });
// If asset is public, allow all authenticated users immediately
if (asset.isPublic && req.user) {
req.asset = asset;
return next();
}
// Otherwise, check org membership
if (asset.orgId === req.user.orgId) {
req.asset = asset;
return next();
}
res.status(403).json({ error: 'Forbidden' });
}On the surface, it looks logical: public assets should be available to any logged-in user. The mistake is that this middleware gets applied not only to GET routes, but also to PUT, PATCH, DELETE, POST — any operation. The next() call passes the request through without checking whether the user should be allowed to modify the resource. The public flag becomes a skeleton key.
The fix isn't just to add an operation-specific check in the middleware, but to fundamentally decouple visibility from permission. Public assets should still carry an ACL, or at minimum an immutable public policy that forbids writes from non-members.
A corrected approach would separate concerns:
// Better: separate visibility from authorization
async function assetAccessControl(req, res, next) {
const asset = await db.findAsset(req.params.assetId);
if (!asset) return res.status(404);
// Visibility: anyone (even anonymous) can read if public
if (req.method === 'GET' && asset.isPublic) {
req.asset = asset;
return next();
}
// For non-GET or private assets, enforce org membership
if (asset.orgId === req.user.orgId) {
req.asset = asset;
return next();
}
res.status(403);
}Even this is simplified. In a real system, you'd have roles like editor, viewer, admin. But the key point: "public" doesn't override write restrictions.
The Authorization Flow, Visualized
Here's how the broken flow compares to the fixed one when a cross-org user tries to modify a public asset:
The sequence makes it obvious: the mere presence of a public flag short-circuits all authorization logic in the first case. That's the trap.
How to Detect This in Your Own APIs
When I'm doing a security review or bug bounty hunting on a multi-tenant application, this is one of the first things I test. The approach is straightforward:
- Create two separate organizations (or tenants) in the application.
- In Org A, create an asset and mark it as public.
- As a user from Org B (with no connection to Org A), attempt to access that asset.
- Can you read it? (That might be expected.)
- Can you modify it? Send a PUT, PATCH, or DELETE request.
- Can you perform actions like comment, share, or trigger workflows?
- Repeat for different asset types: documents, dashboards, reports, anything with a public toggle.
The tooling is simple. You can use Burp Suite's Repeater, or just curl:
# Authenticate as Org B user, get token
TOKEN_B=$(curl -s -X POST https://api.target.com/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"userB@orgb.com","password":"..."}' | jq -r '.token')
# Fetch public asset from Org A (maybe fine)
curl -H "Authorization: Bearer $TOKEN_B" \
https://api.target.com/assets/asset-123
# Try to update it — this should fail
curl -X PUT -H "Authorization: Bearer $TOKEN_B" \
-H 'Content-Type: application/json' \
-d '{"title":"Hacked"}' \
https://api.target.com/assets/asset-123If that PUT returns 200, you've found a critical vulnerability. In my experience, roughly one in five multi-tenant apps I test has some variant of this. The numbers are worse in platforms that bolt on a "public" feature late in development without retrofitting the authorization model.
I also recommend testing with a fully unauthenticated user when the asset is supposed to be accessible without login. The trick is to check whether the public exposure leaks internal identifiers, metadata, or embedded links that could be used in further attacks. Even a read-only leak can be dangerous when it maps out the tenant's internal structure.
Why DevGuard, Specifically?
I don't know the exact details of DevGuard's internal architecture, but the fact that the advisory mentions "users from a different organization with no membership or role" tells me the isolation boundary was broken. It wasn't just an anonymous-access issue; it was cross-tenant data manipulation. That's a CVSS score in the 9.x range if it allowed modification of vulnerability reports or scan results — imagine an attacker silently altering a public advisory to hide a critical flaw they intend to exploit.
The fix, according to the advisory, likely involved adding an additional check to ensure that even for public assets, only members of the owning organization could perform state-changing operations. In some cases, the fix might also restrict certain read-sensitive metadata (like internal comments) from public view, but the core issue was authorization, not data filtering.
This pattern isn't unique to DevGuard. I've seen identical flaws in:
- Source code hosting platforms: public repos that allowed forced pushes from anyone.
- Wiki systems: public pages editable by any registered user.
- Vulnerability databases themselves: public entries where any authenticated user could submit edits without verification.
- Project management tools: public boards with unrestricted card movement.
The common thread: the "public" toggle was implemented as a visibility control, and the authorization checks didn't differentiate between read and write.
The Deeper Problem: Mental Models of Multi-Tenancy
At its core, this bug class stems from a misunderstanding of what multi-tenancy means. Developers often think of tenancy as a walled garden: inside the garden, everyone is trusted; outside, no one gets in. But the real world has footpaths that cross gardens. A "public" asset is a footpath. It allows outsiders to peer in, but it shouldn't let them dig up the flower beds.
The correct mental model is to treat every request as an actor operating on an object in a tenancy context, and to evaluate permissions based on all three, not just one. The public flag modifies the visibility of the object, but the actor's permissions should still be checked against the object's ACL, which might have a role like "viewer" for public users and "editor" for org members.
This isn't just a theoretical exercise. Implementing it properly requires discipline in the authorization layer. Whether you're using OPA, Casbin, or a custom rules engine, the policy must explicitly state: "Allow read if public OR if user is member. Allow write only if user is member with write permission." The disjunctive "public" can't become a universal passthrough.
Actionable Fix Patterns for Developers
If you're building or maintaining a multi-tenant platform, here's what you should do today:
- Audit every endpoint that handles a resource with a `public` or `visibility` flag. Map out the operations allowed on that resource type (CRUD, share, comment, etc.) and verify that the authorization check is performed for each operation independently, not bypassed based on the flag alone.
- Separate the concepts of "visibility" and "access" in your codebase. Create distinct functions like
canView(user, resource)andcanEdit(user, resource). The former may return true for public resources; the latter should not, unless the user has explicit edit rights. - Enforce authorization at the data access layer, not just in middleware. If you use an ORM, consider using row-level security in the database to add an organization/tenant check that's impossible to bypass in application code. For example, PostgreSQL Row Security Policies can ensure that updates to a table always filter by
org_id = current_setting('app.current_org_id')unless the user has a special override role. - Implement integration tests that cross tenant boundaries. Your test suite should include scenarios where a user from Org B tries to mutate a public asset from Org A. If the test doesn't fail, your authorization is broken. Automate this and run it in CI.
- Apply the principle of least privilege to public assets. Even read access might need scrutiny. Are there internal notes, draft versions, or sensitive metadata attached to the public asset that shouldn't be exposed? Filter them at the API layer, not just in the UI.
- Use a centralized authorization service. Hardcoding
if (asset.isPublic)in multiple controllers is a recipe for inconsistency. A centralized policy evaluation engine (like OPA with Rego) can enforce that "public-read" and "member-write" are handled consistently across all services.
Here's how you might express that policy in Rego:
package authz
import data.assets
default allow = false
allow {
input.method == "GET"
asset := assets[input.asset_id]
asset.is_public == true
}
allow {
asset := assets[input.asset_id]
input.user.org_id == asset.org_id
input.user.role == "editor"
}Notice that the write operation (any non-GET) never routes through the public flag alone. It always requires org membership and an editor role. That's the correct posture.
What Governance Looks Like on the Defensive Side
For security teams managing a DevGuard deployment or similar tools, the immediate task is patching. But beyond that, consider these monitoring strategies:
- Create canary assets: Artificially create a public asset that no legitimate user would ever modify. Set up alerts on any update to that asset, and tag the event with the requesting user's org context. If you see an update from a foreign org, you've caught either a breach or a misconfiguration.
- Audit access logs retroactively: Search for PUT/PATCH/DELETE operations on public assets performed by users whose
org_iddoesn't match the asset's owner. This can uncover historical exploitation attempts. - Enforce a policy that public assets require a separate API key or scope for write access. This way, even if an authorization bug slips through, the damage surface is limited.
The Bigger Picture: API Security Testing in the Age of Public APIs
This isn't just about internal tools. Many companies expose public APIs with "public" endpoints that still require authentication for certain actions. I've seen API gateways misconfigured to require authentication on a public endpoint, but then behind the gateway, the service assumes any authenticated request is fully authorized within the tenant's scope, because it never expected an external tenant's user to reach that service. The result? Cross-tenant access.
The lesson: every API that handles multi-tenant data must be explicitly tenant-aware at every layer. You can't rely on the network or authentication proxy to enforce isolation. The application itself must check org_id on every operation.
Wrapping Up: Public Is a Permission, Not an Exemption
The DevGuard advisory is a reminder that the simplest features can become the most dangerous when their implications aren't fully thought through. A toggle labeled "Make Public" sounds harmless. But in a multi-tenant system, it introduces a footpath across security boundaries, and if the authorization model collapses around it, the results can be catastrophic.
Next time you're reviewing code and see:
if (resource.isPublic) {
// short-circuit all checks
}Stop. Ask: "What operations are we allowing? Is write access really intended for the entire world?" The answer is almost always no.
Treat public as a role, not an absence of rules. It's a subtle shift in thinking, but it makes the difference between a solid multi-tenant architecture and a ticking time bomb.
Now, go audit your public endpoints. There's a good chance you'll find something you didn't expect. I usually do.
Related posts
- Security
How I got free cinema credit by ordering -2 popcorns
A missing input validation on M-Tix Cinema XXI's food ordering API let me increase my account balance by submitting negative quantities. No tools needed — just a browser.
May 19, 2026 · 6 min - Security
How I analyze API security headers in 30 seconds
A quick checklist for reading HTTP response headers and spotting security misconfigurations before you even look at the response body.
May 18, 2026 · 7 min - Security
Common auth mistakes I find when reverse-engineering APIs
After years of poking at APIs that weren't meant to be poked at, these are the auth patterns that break most often — and why.
May 18, 2026 · 9 min