Skip to content

fix: prevent pending stream cache exhaustion and stream eviction - #158

Merged
Anushreebasics merged 7 commits into
itzzavdhesh:mainfrom
priyansh13-c:issue-152
Jun 18, 2026
Merged

fix: prevent pending stream cache exhaustion and stream eviction#158
Anushreebasics merged 7 commits into
itzzavdhesh:mainfrom
priyansh13-c:issue-152

Conversation

@priyansh13-c

@priyansh13-c priyansh13-c commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

🚀 Program

GSSoC

📝 Description

Fixes a Denial of Service (DoS) vulnerability in the voice generation flow caused by uncontrolled allocation of entries in the pendingStreams cache.

Previously, the /api/voice/speak endpoint could create pending stream entries before sufficient validation, allowing attackers to flood the cache with requests using invalid ElevenLabs API keys. Once the cache reached PENDING_STREAMS_MAX, legitimate users' pending speech streams could be evicted before retrieval.

This PR introduces safeguards to prevent cache exhaustion from abusive requests and ensures that legitimate pending streams are not evicted due to malicious traffic.

🔗 Related Issue

Closes #152

🔄 Type of Change

  • 🐛 Bug fix

🧪 How to Test

  1. Start the backend and frontend:

    npm run dev:server
    npm run dev:client
  2. Open http://localhost:5173 in a browser.

  3. Use the app's TTS flow to generate a speech request with a valid X-ElevenLabs-Api-Key.

  4. Confirm the first requests succeed and return a speechId/audio URL.

  5. To verify the fix, send enough /api/voice/speak requests to fill the pending stream cache:

    • If using default settings, this means around PENDING_STREAMS_MAX concurrent pending requests.
    • If you want a quicker test, set PENDING_STREAMS_MAX=10 in the backend environment.
  6. Once the cache is full, further /speak requests should return:

    HTTP 503
    error: Too many pending speech requests. Please retry after retrieving or cancelling existing audio streams.
    
  7. Confirm existing pending stream URLs still work and are not evicted.

  8. Run the automated backend tests:

    npm test --workspace server

📸 Screenshots (if applicable)

N/A (Backend security fix)

✅ Checklist

  • I am contributing under GSSoC, NSOC, SSOC, or ELUSOC
  • My code follows the project's existing style
  • I have tested my changes in a browser
  • I have linked the related issue above
  • My PR title follows Conventional Commits format (e.g. feat: add voice preview)

Summary by cubic

Prevents DoS in TTS by rejecting new /api/voice/speak requests once the pending stream store hits capacity, without evicting active streams. Adds secure speechIds using crypto.randomUUID() and configurable TTL cleanup to address #152.

  • Bug Fixes
    • Early capacity check returns 503 before API key validation or creating a pending entry.
    • Configurable limits and TTL via PENDING_STREAMS_MAX and PENDING_STREAM_TTL_MS; pending entries auto-expire, deletePendingStream clears unref’ed timers.
    • Trims text/voice_id and clamps voice_settings with safe defaults; tests cover overflow rejection, oldest/newest streams still completing, and speechId in audio URLs.

Written for commit b2bc910. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Added configurable limits for queued speech requests; when the queue reaches capacity, new /speak requests are rejected immediately with HTTP 503 and a clear JSON error, while already queued speeches continue streaming.
    • Improved voice settings handling by sanitizing and clamping user-provided values into safe, bounded ranges before processing, and added automatic cleanup for pending items via a TTL.
  • Tests

    • Updated the capacity/TTL test to confirm rejection precisely at the maximum pending limit and to verify previously queued speeches (oldest and newest) still complete successfully.

@vercel

vercel Bot commented Jun 7, 2026

Copy link
Copy Markdown

@priyansh13-c is attempting to deploy a commit to the itzzavdhesh's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

✍️ DCO Sign-off Needed

Hey @priyansh13-c! 👋 One or more commits in this PR are missing a Signed-off-by: line.

Warning

  • 25b9566 Fixed DoS Vulnerability
  • 121225e Merge branch 'main' into issue-152
  • 08e549b Merge branch 'main' into issue-152
  • ccb1047 Update server/controllers/voiceController.js
  • 09a5d09 Refactor voiceController.js to streamline pending streams
  • b3cbcf3 Refactor speechId generation to use crypto module
  • b2bc910 Update server/controllers/voiceController.js

How to fix:

For the latest commit:

git commit --amend --signoff
git push --force-with-lease

For multiple commits, replace N with the number to update:

git rebase --signoff HEAD~N
git push --force-with-lease

This comment will update automatically after you push.


🤖 VoiceForge Automation · Updates automatically on edits

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6aa84f7-af63-46a3-82db-79ff79a381af

📥 Commits

Reviewing files that changed from the base of the PR and between b3cbcf3 and b2bc910.

📒 Files selected for processing (1)
  • server/controllers/voiceController.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/controllers/voiceController.js

📝 Walkthrough

Walkthrough

The PR modifies the pending stream capacity enforcement in the /speak endpoint. Instead of evicting the oldest pending streams when the cache reaches PENDING_STREAMS_MAX, the endpoint now immediately rejects excess requests with HTTP 503. Voice settings are sanitized and merged with defaults during request preparation. The test suite is updated to verify the new rejection behavior and confirm that queued streams remain available under capacity constraints.

Changes

Pending Stream Capacity Enforcement and Rejection

Layer / File(s) Summary
Configuration and in-memory pending stream store
server/controllers/voiceController.js
Adds environment-configurable constants PENDING_STREAMS_MAX and PENDING_STREAM_TTL_MS to bound pending speech request capacity and cleanup timing. Introduces module-level pendingStreams Map for tracking queued speech requests and deletePendingStream() helper to remove entries while clearing their associated cleanup timers.
Speak endpoint capacity guard and request handling
server/controllers/voiceController.js
The speak handler adds an early capacity guard that returns HTTP 503 with JSON error when pendingStreams.size reaches PENDING_STREAMS_MAX. Request preparation sanitizes and clamps user-provided voice_settings fields into bounded ranges, merges them with default settings, generates a new speechId, schedules TTL cleanup via setTimeout, and stores the pending entry in pendingStreams with merged settings and cleanup timer reference.
Test validation of overflow rejection and stream retention
server/test/voiceController.secure-id.test.js
Test configures a small pending stream capacity limit (10) and long TTL via environment variables, fills the pending store to exactly capacity with 10 /speak requests, verifies that an additional /speak call is rejected with HTTP 503 and the specific error message, and asserts that both the oldest and newest pending entries remain available for streaming instead of being evicted.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • itzzavdhesh/VoiceForge#108: Both PRs modify server/controllers/voiceController.js's /speak pending-stream handling—adding/bounding PENDING_STREAMS_MAX and per-entry TTL cleanup via pendingStreams plus associated speechId lifecycle logic.
  • itzzavdhesh/VoiceForge#35: Both PRs modify server/controllers/voiceController.js around the TTS streaming flow—specifically speak's handling of the pendingStreams store and speechId lifecycle.
  • itzzavdhesh/VoiceForge#120: The main PR extends in-memory pendingStreams Map with capacity and TTL eviction, while the retrieved PR removes that pendingStreams state entirely in favor of stateless encrypted tokens.

Suggested labels

type:security, type:testing, level:intermediate

Suggested reviewers

  • Anushreebasics
  • itsdakshjain
  • sabeenaviklar

Poem

🐰 A hop and a skip through the cache so grim,
Where streams used to vanish when defenses grew slim,
But now guard the fort with a steadfast "503,"
Let queued voices stay safe for all users to see!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preventing pending stream cache exhaustion and stream eviction through capacity guards.
Linked Issues check ✅ Passed The PR addresses issue #152 by implementing a pre-capacity check that returns HTTP 503 when cache is full, preventing both exhaustion and eviction of legitimate streams.
Out of Scope Changes check ✅ Passed All changes are scoped to voiceController.js and its test file, directly addressing the DoS vulnerability without introducing unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

🎉 PR Ready for Mentor Review

Hey @priyansh13-c! 👋 Your PR passed all checks and is now in the GSSoC review queue.

Note

🔗 Closing: #152 · 📐 134 lines across 2 file(s) · 📬 Already requested or no eligible reviewer found

@sabeenaviklar @Anushreebasics @itsdakshjain @snehkris @1754riya @Mrigakshi-Rathore @Itzzavdheshh @vedhapprakashni, this PR is ready for your review — please confirm scope, check behavior and tests, then approve or request changes.

Important

This is not an approval. Please wait for mentor feedback before expecting a merge. If changes are requested, push them to this same branch and keep the PR focused on the linked issue.


🤖 VoiceForge Automation · Updates automatically on edits

@priyansh13-c priyansh13-c changed the title Fixed DoS Vulnerability fix: prevent pending stream cache exhaustion and stream eviction Jun 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/test/voiceController.secure-id.test.js (1)

87-104: 💤 Low value

Well-structured validation of non-eviction behavior.

The test correctly verifies that both the oldest and newest pending stream entries remain accessible when the cache is at capacity, confirming that the new implementation does not evict existing streams. This directly validates the fix for the eviction aspect of the DoS vulnerability.

Note: The test does not cover the scenario where invalid ElevenLabs API keys are rejected before creating pending stream entries, because that validation is not yet implemented (see comment on voiceController.js:103-109).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/test/voiceController.secure-id.test.js` around lines 87 - 104, The
test notes that invalid ElevenLabs API keys are not being rejected before
creating pending stream entries; update the validation logic in
voiceController.js (around the code handling creation of pending streams
referenced at voiceController.js:103-109) to verify the ElevenLabs API key early
and throw or return an error before adding any pending stream entry, ensuring
functions that create or enqueue streams (e.g., the handler that calls
createPendingStreamEntry/streamSpeech) validate the key, respond with an
appropriate error, and never create pending entries when the key is invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/controllers/voiceController.js`:
- Around line 103-109: The pendingStreams DoS risk arises because requireApiKey
only checks for non-empty X-ElevenLabs-Api-Key and speak stores that key into
pendingStreams without verifying it; to fix, validate the provided ElevenLabs
key (e.g., by calling a lightweight ElevenLabs auth/validate endpoint or reusing
an existing verification helper) before inserting into pendingStreams in the
speak handler, cache successful key validations (TTL tied to
PENDING_STREAM_TTL_MS) to avoid repeated validation, and add rate limiting
middleware for the /api/voice/speak route (by IP and by API key) so malicious
clients cannot exhaust PENDING_STREAMS_MAX — update the functions/variables:
requireApiKey, speak, streamSpeech, pendingStreams, PENDING_STREAMS_MAX, and
PENDING_STREAM_TTL_MS accordingly.

---

Nitpick comments:
In `@server/test/voiceController.secure-id.test.js`:
- Around line 87-104: The test notes that invalid ElevenLabs API keys are not
being rejected before creating pending stream entries; update the validation
logic in voiceController.js (around the code handling creation of pending
streams referenced at voiceController.js:103-109) to verify the ElevenLabs API
key early and throw or return an error before adding any pending stream entry,
ensuring functions that create or enqueue streams (e.g., the handler that calls
createPendingStreamEntry/streamSpeech) validate the key, respond with an
appropriate error, and never create pending entries when the key is invalid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c2904b80-2c53-4a0f-b42f-b80a9e3103ff

📥 Commits

Reviewing files that changed from the base of the PR and between 16292ef and 25b9566.

📒 Files selected for processing (2)
  • server/controllers/voiceController.js
  • server/test/voiceController.secure-id.test.js

Comment thread server/controllers/voiceController.js
@itsdakshjain itsdakshjain added the mentor:itsdakshjain GSSoC: Mentor-@itsdakshjain label Jun 7, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread server/controllers/voiceController.js

@itsdakshjain itsdakshjain left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve the comments

@github-actions github-actions Bot removed the mentor:leonagoel GSSoC: Mentor-@leonagoel label Jun 8, 2026
@Anushreebasics Anushreebasics added level:beginner level:beginner quality:clean quality:clean labels Jun 9, 2026
@itzzavdhesh

Copy link
Copy Markdown
Owner

@priyansh13-c please resolve the branch conflicts

@priyansh13-c

Copy link
Copy Markdown
Contributor Author

@itzzavdhesh resolving conflicts for the third time, please merge the pr

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/controllers/voiceController.js (2)

284-284: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

randomUUID() is not defined — runtime ReferenceError.

randomUUID is called without qualification, but it's not imported. It should be crypto.randomUUID().

🐛 Proposed fix
-    const speechId = randomUUID();
+    const speechId = crypto.randomUUID();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/controllers/voiceController.js` at line 284, The randomUUID function
is being called without the crypto module qualifier, causing a runtime
ReferenceError. In the voiceController.js file where speechId is assigned using
randomUUID(), change the unqualified randomUUID() call to crypto.randomUUID() to
properly reference the function from the Node.js crypto module. Ensure the
crypto module is already imported at the top of the file, or add the import if
missing.

201-207: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

PENDING_STREAMS_MAX is undefined — runtime ReferenceError.

The capacity guard uses PENDING_STREAMS_MAX but this constant is never defined or imported. The test sets process.env.PENDING_STREAMS_MAX, so you need to read it from the environment.

🐛 Proposed fix: Add constant definitions at module level
 const ALGORITHM = "aes-256-gcm";
+
+const PENDING_STREAMS_MAX = parseInt(process.env.PENDING_STREAMS_MAX, 10) || 100;
+const PENDING_STREAM_TTL_MS = parseInt(process.env.PENDING_STREAM_TTL_MS, 10) || 60000;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/controllers/voiceController.js` around lines 201 - 207, The constant
PENDING_STREAMS_MAX is referenced in the capacity guard check (if statement
comparing pendingStreams.size >= PENDING_STREAMS_MAX) but is never defined,
causing a runtime ReferenceError. Add a constant definition at the module level
in voiceController.js that reads PENDING_STREAMS_MAX from the environment
variable process.env.PENDING_STREAMS_MAX with a sensible default fallback value
to ensure the constant is available when the guard check executes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/controllers/voiceController.js`:
- Around line 298-299: The token expiry calculation on line 298 hardcodes 60000
milliseconds instead of using the PENDING_STREAM_TTL_MS environment variable
that is already being used elsewhere for the pending stream cleanup timeout.
Replace the hardcoded 60000 value in the expiresAt calculation with the
PENDING_STREAM_TTL_MS constant to ensure the token expiry and stream cleanup
timeout remain synchronized, preventing divergence when the TTL is configured
differently via environment variables.
- Around line 208-212: Remove the redundant if block (lines 208-211) that calls
requireApiKey(request) without capturing its result. Keep only the ternary
assignment on line 212 that properly assigns the result of the conditional
requireApiKey call to the apiKey variable. This eliminates the duplicate
function call and clarifies the flow.
- Around line 292-293: The variable `apiKey` is being redeclared with `const` at
line 292, but it was already declared at line 212 in the same scope, causing a
SyntaxError. Remove the `const` keyword and simply assign the value to the
existing `apiKey` variable. Additionally, when storing the pending stream data
in `pendingStreams.set()` at line 293, use the trimmed versions of `text` and
`voiceId` instead of the original values to maintain consistency with the
trimming that has already been performed earlier in the function.

---

Outside diff comments:
In `@server/controllers/voiceController.js`:
- Line 284: The randomUUID function is being called without the crypto module
qualifier, causing a runtime ReferenceError. In the voiceController.js file
where speechId is assigned using randomUUID(), change the unqualified
randomUUID() call to crypto.randomUUID() to properly reference the function from
the Node.js crypto module. Ensure the crypto module is already imported at the
top of the file, or add the import if missing.
- Around line 201-207: The constant PENDING_STREAMS_MAX is referenced in the
capacity guard check (if statement comparing pendingStreams.size >=
PENDING_STREAMS_MAX) but is never defined, causing a runtime ReferenceError. Add
a constant definition at the module level in voiceController.js that reads
PENDING_STREAMS_MAX from the environment variable
process.env.PENDING_STREAMS_MAX with a sensible default fallback value to ensure
the constant is available when the guard check executes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 68b2374a-469d-4aa8-b5da-fcd8d8b89b6d

📥 Commits

Reviewing files that changed from the base of the PR and between 25b9566 and 08e549b.

📒 Files selected for processing (2)
  • server/controllers/voiceController.js
  • server/test/voiceController.secure-id.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/test/voiceController.secure-id.test.js

Comment thread server/controllers/voiceController.js Outdated
Comment thread server/controllers/voiceController.js Outdated
Comment thread server/controllers/voiceController.js
priyansh13-c and others added 2 commits June 15, 2026 22:01
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@priyansh13-c

Copy link
Copy Markdown
Contributor Author

@itsdakshjain @itzzavdhesh @Anushreebasics CI is failing during the frontend build (Build client). My changes are limited to server/controllers/voiceController.js and related tests. I also checked a recent workflow run on main (CI #476), which is failing during the same build stage. Could a maintainer confirm whether this is a pre-existing issue?
image
in client/hooks/useSpeechHistory.js entry has been declared twice.

@itsdakshjain itsdakshjain left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even thought the comments are marks as resolve, they are still not solved
Please add module-level declarations for PENDING_STREAMS_MAX, PENDING_STREAM_TTL_MS, and import crypto to prevent runtime error
Or if you think solved justify it

@itsdakshjain

Copy link
Copy Markdown
Collaborator

@itsdakshjain @itzzavdhesh @Anushreebasics CI is failing during the frontend build (Build client). My changes are limited to server/controllers/voiceController.js and related tests. I also checked a recent workflow run on main (CI #476), which is failing during the same build stage. Could a maintainer confirm whether this is a pre-existing issue? image in client/hooks/useSpeechHistory.js entry has been declared twice.

@priyansh13-c You are completely correct that your PR didn't cause the frontend client build failure (useSpeechHistory.js). That's an upstream issue on main and you aren't responsible for fixing it here.

However, the review comment is about a separate backend runtime error inside your changes to server/controllers/voiceController.js.

In commit 7c0fd96, the capacity guard logic uses PENDING_STREAMS_MAX and randomUUID(). But because PENDING_STREAMS_MAX is never declared from process.env and the native crypto module isn't imported at the top of the file, Node.js will throw a ReferenceError and crash the server when the /speak endpoint is hit in production.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread server/controllers/voiceController.js Outdated
@priyansh13-c

Copy link
Copy Markdown
Contributor Author

@itsdakshjain i have fixed the issue, please ask the admin to fix client CI issue and merger it, also can you change the level to intermediate

@itsdakshjain

Copy link
Copy Markdown
Collaborator

@itsdakshjain i have fixed the issue, please ask the admin to fix client CI issue and merger it, also can you change the level to intermediate

Pls resolve the issue regarding pending stream max , and the ci check will be handled on our end , that is not blocking your pr

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
@priyansh13-c

Copy link
Copy Markdown
Contributor Author

@itsdakshjain done

@Anushreebasics
Anushreebasics merged commit 7d80567 into itzzavdhesh:main Jun 18, 2026
8 of 10 checks passed
@github-actions

Copy link
Copy Markdown

🎊 PR Merged Successfully

Hey @priyansh13-c! 👋 Congratulations and thank you for your contribution to VoiceForge!

Note

🔗 Linked issue(s): #152 · ✅ Marked as merged and complete

Maintainers may still handle final cleanup, release notes, or follow-up tracking after the merge.


🤖 VoiceForge Automation · Updates automatically on edits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]DoS Vulnerability via Cache Eviction (voiceController.js)

5 participants