July 6, 2026
Loose Lips and Open Ears: Chaining API Bugs to Full Access
It started with a harmless-looking folder ID. A few requests later, we were holding a guest token for a project that did not belong to us…

By Jan Urech
5 min read
It started with a harmless-looking folder ID. A few requests later, we were holding a guest token for a project that did not belong to us. What looked like noisy API design turned into full cross-project access by chaining predictable IDs, duplicated identifiers, weak authorization checks, and one overly helpful response body.
As usual during our web application penetration tests, we started by exploring and enumerating the web application. The application in question, let's call it AppA, allows users to create projects. Each project can be assigned to multiple users who authenticate to the web app through SSO. In each project, users fill out a series of forms and upload files to apply for a certificate.
AppA also needs to support people outside the organization who do not have access to SSO. For this alternative authorization flow, guests can use a secret token to access a project. That token would later become the missing link between a folder reference bug and full cross-project access.
Web application designs start with good intentions. Authentication should be secure, centralized, and handled through SSO. Then reality happens. An external user needs access. A partner system needs an integration. A support process needs a shortcut. Before long, there is a second way in, and that second way often does not come with the same security guarantees as the first. That is why alternative authentication flows are always worth a closer look during penetration tests.
While exploring AppA's basic functionality, one design choice stood out quickly. The API was extremely verbose. Requests and responses often contained far more data than the feature on screen needed. That may sound harmless, but every unnecessary field is another piece of data that has to be handled, validated, and authorized correctly.
For example, when interacting with user objects, the application did not just return the basic information required for the current view. Even when only a username was needed, the server returned a much larger user object with additional fields that were not used in that context.
To be clear, at this stage we were not receiving data we were forbidden to access. The problem was more subtle. AppA kept exposing and accepting data that was irrelevant to the current action. That gave us more places to look for mismatches between what the client could send and what the server processed.
The same pattern appeared when writing data. AppA was verbose not only in GET requests, but also in PUT and POST requests. Instead of sending only the fields that changed, the client often sent the full object it had previously received from the server. If we changed a username, for example, the request still included a large amount of unrelated user information. Naturally, we started modifying fields that the UI did not normally allow us to change. Most of the time, the server simply ignored them.
The hunt was on. We started testing requests for unexpected reads and writes, and one pattern quickly caught our attention. When updating a project, the project ID appeared twice. It was present once in the request path and again in the request body. This is always worth investigating, because duplicated identifiers can lead to confusing trust boundaries. An application might authorize the request based on one value, but then use the other value when deciding which object to modify. In AppA's case, the PID was also system-assigned and could not be changed through the UI, which made its presence in the request body even more suspicious.
PUT /AppA/project/0001-p
[HEADERS]
{
"id": 1337,
"pid": "0001-p",
[...many more fields]
}PUT /AppA/project/0001-p
[HEADERS]
{
"id": 1337,
"pid": "0001-p",
[...many more fields]
}At first, the impact looked like denial of service. By changing the PID in both the request path and body of a PUT request, we could update the PID of one of our assigned projects to '0123-p' which collided with another existing project's PID.
PUT /AppA/project/0123-p
[HEADERS]
{
"id": 1337,
"pid": "0123-p",
[...many more fields]
}PUT /AppA/project/0123-p
[HEADERS]
{
"id": 1337,
"pid": "0123-p",
[...many more fields]
}After that request, both our project and the project with the PID '0123-p' became inaccessible and started returning errors. That was already a valid DoS finding, but it also pointed to something more important. AppA trusted client-supplied identifiers in places where it should not. If the same pattern affected other references, the impact might go beyond availability.
The next obvious target was file storage. AppA lets users upload files as part of their project forms, and those files are stored in a project-specific folder. Each project object references its folder by a folder ID. We tried changing that reference to point to a folder belonging to another project.
PUT /AppA/project/0001-p
[HEADERS]
{
"id": 1337,
"pid": "0001-p",
"folderId": "1234",
[...many more fields]
}PUT /AppA/project/0001-p
[HEADERS]
{
"id": 1337,
"pid": "0001-p",
"folderId": "1234",
[...many more fields]
}The first attempt failed with a 500 Internal Server Error. We saw the same behavior when changing only the PID in the body, and again when changing both the path PID and body PID to the foreign project's ID.
The behavior changed when only the PID in the path was modified while the body still referenced our own project. That request returned a 200 OK response.
PUT /AppA/project/0123-p
[HEADERS]
{
"id": 1337,
"pid": "0001-p",
"folderId": "1234",
[...many more fields]
}PUT /AppA/project/0123-p
[HEADERS]
{
"id": 1337,
"pid": "0001-p",
"folderId": "1234",
[...many more fields]
}After this request, the GUI started showing an error when we tried to open our project's folder. Still, the association had clearly changed. Our project now appeared to reference the folder with ID '1234', which belonged to another project. We could see the uploaded file names, but attempting to open the files returned a 403 Forbidden error.
That partial access was enough to keep digging. When we inspected the folder response directly, the verbose API pattern appeared again. The response contained far more information than was needed to display or interact with the folder.
200 OK
[HEADERS]
{
"name":"MyFolder",
"pid": "0123-p",
"empty": false,
"token": "SecretToken123",
[...many more fields]
}200 OK
[HEADERS]
{
"name":"MyFolder",
"pid": "0123-p",
"empty": false,
"token": "SecretToken123",
[...many more fields]
}This was no longer just metadata leakage. The response handed us the token used by external guests to access that project!
That token was the turning point. By using it, we could enter the foreign project through the intended guest-access flow and interact with its data as an authorized external guest. In practice, the folder reassignment had become a bridge from partial metadata exposure to cross-project access.
That left one practical question. How would an attacker find project and folder IDs that did not belong to them?
In AppA, both values were incremental. Once we knew our own project and folder IDs, we could work outward from those numbers and test nearby values until another valid project or folder reference appeared. Enumeration was not the only option either. Some endpoints returned project and folder IDs directly, giving us another way to collect the identifiers needed for the chain.
In summary, the vulnerability was not caused by one spectacularly broken endpoint. It came from the way several smaller weaknesses reinforced each other, including redundant identifiers, insufficient authorization checks on referenced objects, predictable project and folder IDs, and responses that returned sensitive data outside the immediate need of the feature. By chaining these issues, we could associate another project's folder with our own project, retrieve its guest token, and use that token to enter the foreign project as an external guest.
Preventing this kind of issue requires treating authorization, object references, and response design as one connected system. Applications should enforce object-level authorization on every referenced resource, avoid accepting duplicated or conflicting identifiers from the client, and rely on server-side ownership relationships instead of trusting user-controlled fields. APIs should also follow the principle of least privilege by returning only the data required for the current action, keeping sensitive tokens out of unrelated responses, and avoiding predictable identifiers where guessing or enumeration would expose additional access paths.
The guest token feature worked exactly as designed. Just not only for the intended guests.