Back to skill

Security audit

Discord Voice Using Deepgram

Security checks for vulnerabilities and agentic risk

Overview

This Discord voice skill largely does what it claims, but it needs review because unintended speakers may be captured, transcripts are logged, and voice prompts can reach the agent's full toolset.

Review this before installing on any server with sensitive conversations or powerful agent tools. Prefer Discord user IDs over names, set an explicit allowlist, disable auto-join and voice speaker switching unless needed, isolate the voice agent from high-impact tools, and remove or tightly restrict transcript logging.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
src/voice-connection.ts:886
Finding
Speaker Authorization Fails Open When the Primary User Cannot Be Resolved<![CDATA[ ## Vulnerability Details **File Location**: `src/voice-connection.ts:886-900` and `src/voice-connection.ts:947-958` **Vulnerability Type**: Fail-open access control **Risk Level**: High ### Vulnerable Code ```ts private resolvePrimaryUser(session: VoiceSession): void { if (!this.config.primaryUser) return; const id = this.resolveUserId(session, this.config.primaryUser); if (id) { session.primaryUserId = id; this.logger.info(`[discord-voice] Primary speaker resolved to userId=${id}`); } else { // If we can't resolve (name not in channel yet), we keep primaryUserId unset. // The user can re-join / or say the switch command after they join. this.logger.warn(`[discord-voice] Could not resolve primaryUser="${this.config.primaryUser}" in this voice channel yet.`); } } ``` ```ts private isUserAllowed(session: VoiceSession, userId: string): boolean { // If primaryUser is set, default to listening ONLY to them. // If activeSpeakerId is set, listen to primary + active. if (session.primaryUserId) { if (userId === session.primaryUserId) return true; if (session.activeSpeakerId && userId === session.activeSpeakerId) return true; return false; } // Otherwise fall back to allowedUsers list (user IDs) if (this.config.allowedUsers.length === 0) return true; return this.config.allowedUsers.includes(userId); } ``` ### Technical Analysis The authorization decision is based on whether `session.primaryUserId` was successfully resolved, rather than whether the administrator configured `primaryUser`. If the configured primary user cannot be found in the current channel, `resolvePrimaryUser()` leaves `session.primaryUserId` undefined. This can occur when: - The primary user has not joined the channel. - A name-based selector no longer matches the user's current name. - The configured name is ambiguous or misspelled. - The expected member is temporarily unavailable when the bot joins. When `primaryUserId` is und ...[truncated 1856 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Distinguish between an unconfigured primary user and a configured but unresolved primary user: ```ts private isUserAllowed(session: VoiceSession, userId: string): boolean { if (this.config.primaryUser) { if (!session.primaryUserId) { return false; } return userId === session.primaryUserId || userId === session.activeSpeakerId; } if (this.config.allowedUsers.length === 0) { return false; // Prefer deny-by-default } return this.config.allowedUsers.includes(userId); } ``` 2. Do not start audio listeners until a configured primary user has been resolved successfully. 3. Prefer immutable Discord user IDs over usernames or display names. Validate ID-based configuration during startup. 4. If name-based selectors remain supported, require an exact and unique match. Reject partial or ambiguous matches. 5. Make unrestricted listening an explicit configuration option rather than interpreting an empty allowlist as authorization for everyone. 6. Emit a clear operational error when access-control configuration cannot be resolved, without silently degrading to unrestricted capture. 7. Add automated tests covering: - Configured and resolved primary user. - Configured but absent primary user. - Renamed primary user. - Empty and non-empty allowlists. - Ambiguous display-name matches. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/voice-connection.ts:541
Finding
Discord Voice Transcripts and User Identifiers Are Written to Application Logs<![CDATA[ ## Vulnerability Details **File Location**: `src/voice-connection.ts:541-545`, `src/voice-connection.ts:665`, and `index.ts:181` **Vulnerability Type**: Sensitive information exposure through logging **Risk Level**: Medium ### Vulnerable Code ```ts const streamingSession = this.streamingSTT.getOrCreateSession(userId, (text, isFinal) => { if (isFinal) { this.logger.debug?.(`[discord-voice] Streaming transcript (final): "${text}"`); } else { this.logger.debug?.(`[discord-voice] Streaming transcript (interim): "${text}"`); } }); ``` ```ts this.logger.info(`[discord-voice] Transcribed: "${transcribedText}"`); ``` ```ts async function handleTranscript( userId: string, guildId: string, channelId: string, text: string ): Promise<string> { api.logger.info(`[deepgram-discord-voice] Processing transcript from ${userId}: "${text}"`); ``` ### Technical Analysis The plugin logs full interim and final speech transcripts. The transcript is also logged together with the speaker's Discord user ID in `index.ts`. Voice conversations may contain personal information, private business discussions, authentication data, credentials spoken aloud, or other confidential material. Application logs often have broader access controls and substantially longer retention periods than live voice data. The same transcript can be recorded more than once: - Interim or final streaming transcript at debug level. - Final transcript at information level. - Transcript and Discord user ID when processing begins. Because final transcripts are logged at information level, exposure occurs under ordinary production logging configurations and does not require debug logging to be enabled. ### Attack Path 1. An allowed participant speaks sensitive information in a connected Discord voice channel. 2. The audio is sent to Deepgram and converted into text. 3. The plugin writes the full transcript to the information log. 4. `handleTranscript()` writes the transcript ...[truncated 903 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove transcript bodies from routine information and debug logs. 2. Log only non-content metadata, such as: ```ts this.logger.info( `[discord-voice] Transcript received for user=${redactedUserId}, characters=${transcribedText.length}` ); ``` 3. Redact or hash Discord user IDs before logging unless the raw identifier is strictly required for incident response. 4. If transcript-content logging is needed for troubleshooting, require a separate explicit diagnostic setting that: - Defaults to disabled. - Displays a privacy warning. - Automatically expires. - Is restricted to development environments. - Applies structured redaction before output. 5. Configure log access controls, retention limits, encryption, and deletion policies appropriate for voice-derived personal data. 6. Avoid logging interim streaming transcripts because they can reveal content that is later corrected or excluded from the final transcript. 7. Add tests or static checks preventing raw values such as `text` and `transcribedText` from being passed to production logging calls. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (31)

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding
The finding indicates the skill may dynamically load core agent modules, manage sessions/workspaces, access persistent session stores, and support embedded agent execution—capabilities far beyond a voice plugin's declared role. Such hidden breadth materially increases risk because a plugin installed for speech features may also gain privileged access to agent state, persistence, and execution pathways that could be abused for lateral movement or data exposure.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The finding indicates the skill may dynamically load core agent modules, manage sessions/workspaces, access persistent session stores, and support embedded agent execution—capabilities far beyond a voice plugin's declared role. Such hidden breadth materially increases risk because a plugin installed for speech features may also gain privileged access to agent state, persistence, and execution pathways that could be abused for lateral movement or data exposure.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The finding indicates the skill may dynamically load core agent modules, manage sessions/workspaces, access persistent session stores, and support embedded agent execution—capabilities far beyond a voice plugin's declared role. Such hidden breadth materially increases risk because a plugin installed for speech features may also gain privileged access to agent state, persistence, and execution pathways that could be abused for lateral movement or data exposure.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill declares required environment variables and clearly describes networked Discord/Deepgram behavior, but it does not declare any explicit tool scope such as permissions or allowed-tools. That mismatch weakens policy enforcement and review because the runtime may grant broader env and network access than is transparently documented, increasing the chance of unintended token exposure or outbound communication abuse.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The wake word and example commands indicate that speaker-switching can be controlled by spoken phrases, but the schema text does not define safeguards such as exact speaker verification, confirmation flows, or anti-spoofing constraints. In a Discord voice channel, this can let normal conversation, imitation, replayed audio, or ambiguous name matching trigger authorization changes, causing the bot to listen to or respond to unintended users.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The plugin logs raw transcribed speech content together with the user ID, which can capture sensitive conversations, credentials, or personal data spoken in the channel. Voice-chat context makes this especially risky because participants may not expect full-text retention in logs, and logs are often broadly accessible and long-lived.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The transcript handler explicitly tells the embedded agent it has access to all normal tools and skills, and the commented-out lane restriction suggests prior scoping was removed. In a voice-channel context, any user who can speak to the bot may be able to indirectly trigger unrelated powerful capabilities, creating privilege expansion far beyond speech transcription and TTS.

Intent-Code Divergence

Medium
Confidence
93% confidence
Finding
The inline comment indicates a deliberate decision to remove a lane restriction because it might limit tool access, which weakens an apparent security boundary. In combination with untrusted voice transcripts being sent to the agent, this increases the chance that spoken prompts can reach sensitive capabilities that should not be available from a Discord voice session.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The code sends audio buffers to an external Deepgram service via a WebSocket connection, which is a network operation that transmits user data. While the file has technical comments and some console logging, it does not include any user-facing warning, confirmation, or explicit disclosure that captured audio is being sent off-system.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The function sends the provided text to Deepgram over HTTPS, which is a network operation transmitting user-supplied content to a third party. In this file there is no confirmation prompt, user-facing log/print, or comment/docstring warning that input text will be sent to an external service.

External Transmission

Medium
Category
Data Exfiltration
Content
}

  async transcribe(audioBuffer: Buffer, sampleRate: number): Promise<STTResult> {
    const url = new URL("https://api.deepgram.com/v1/listen");
    url.searchParams.set("model", this.model);
    url.searchParams.set("encoding", "linear16");
    url.searchParams.set("sample_rate", String(sampleRate));
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

  async transcribe(audioBuffer: Buffer, sampleRate: number): Promise<STTResult> {
    const url = new URL("https://api.deepgram.com/v1/listen");
    url.searchParams.set("model", this.model);
    url.searchParams.set("encoding", "linear16");
    url.searchParams.set("sample_rate", String(sampleRate));
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

  async transcribe(audioBuffer: Buffer, sampleRate: number): Promise<STTResult> {
    const url = new URL("https://api.deepgram.com/v1/listen");
    url.searchParams.set("model", this.model);
    url.searchParams.set("encoding", "linear16");
    url.searchParams.set("sample_rate", String(sampleRate));
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The function sends the provided audio buffer to Deepgram over the network for transcription, which is a privacy-relevant operation involving user/system data. While the file has a brief class comment, it does not provide any user-facing disclosure, confirmation, logging, or warning about external transmission of audio content.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The synthesize method sends the provided text to Deepgram's remote API, which can expose user content to a third party. While the code is functionally clear, there is no confirmation prompt, user-facing log, or inline warning indicating that input text leaves the local system.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
This component records Discord voice audio, transcribes it via external STT providers, and generates replies via external TTS providers, but this file contains no mechanism to notify or obtain consent from channel participants before their voice data is sent to third-party services. In a voice-chat context, that creates a real privacy and compliance risk because users may be unknowingly recorded and their speech shared outside Discord, potentially violating server expectations, platform policies, or legal consent requirements.

Vague Triggers

Low
Confidence
83% confidence
Finding
This manifest file describes the skill as a 'Discord voice channel plugin' with STT and TTS capabilities, but it does not indicate what specific commands, contexts, or conditions activate the skill. In a manifest file, the lack of explicit trigger scope can make invocation behavior ambiguous and increase the chance of unintended activation.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"deepgram"
  ],
  "dependencies": {
    "@discordjs/voice": "^0.18.0",
    "discord.js": "^14.16.3",
    "libsodium-wrappers": "^0.7.15",
    "opusscript": "^0.0.8",
Confidence
86% confidence
Finding
Using a caret range for a production dependency allows future installs to resolve to different package versions than were originally tested. That increases supply-chain risk because a compromised or breaking upstream minor/patch release could be pulled into deployments without explicit review.

Unpinned Dependencies

Low
Category
Supply Chain
Content
],
  "dependencies": {
    "@discordjs/voice": "^0.18.0",
    "discord.js": "^14.16.3",
    "libsodium-wrappers": "^0.7.15",
    "opusscript": "^0.0.8",
    "prism-media": "^1.3.5",
Confidence
86% confidence
Finding
Using a caret range for a production dependency allows future installs to resolve to different package versions than were originally tested. That increases supply-chain risk because a compromised or breaking upstream minor/patch release could be pulled into deployments without explicit review.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "@discordjs/voice": "^0.18.0",
    "discord.js": "^14.16.3",
    "libsodium-wrappers": "^0.7.15",
    "opusscript": "^0.0.8",
    "prism-media": "^1.3.5",
    "ws": "^8.18.0"
Confidence
86% confidence
Finding
Using a caret range for a production dependency allows future installs to resolve to different package versions than were originally tested. That increases supply-chain risk because a compromised or breaking upstream minor/patch release could be pulled into deployments without explicit review.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"@discordjs/voice": "^0.18.0",
    "discord.js": "^14.16.3",
    "libsodium-wrappers": "^0.7.15",
    "opusscript": "^0.0.8",
    "prism-media": "^1.3.5",
    "ws": "^8.18.0"
  },
Confidence
86% confidence
Finding
Using a caret range for a production dependency allows future installs to resolve to different package versions than were originally tested. That increases supply-chain risk because a compromised or breaking upstream minor/patch release could be pulled into deployments without explicit review.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"discord.js": "^14.16.3",
    "libsodium-wrappers": "^0.7.15",
    "opusscript": "^0.0.8",
    "prism-media": "^1.3.5",
    "ws": "^8.18.0"
  },
  "clawdbot": {
Confidence
86% confidence
Finding
Using a caret range for a production dependency allows future installs to resolve to different package versions than were originally tested. That increases supply-chain risk because a compromised or breaking upstream minor/patch release could be pulled into deployments without explicit review.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"libsodium-wrappers": "^0.7.15",
    "opusscript": "^0.0.8",
    "prism-media": "^1.3.5",
    "ws": "^8.18.0"
  },
  "clawdbot": {
    "extensions": [
Confidence
91% confidence
Finding
Using an unpinned range for the production WebSocket library is more concerning because network-facing packages are common attack surfaces and may receive security-relevant updates or, conversely, introduce vulnerable versions into fresh installs. In a Discord voice plugin that relies on real-time network communication, dependency drift in this component can directly affect exposed runtime behavior.

Unverifiable Dependency: ws has 7 known advisory(ies) (CVE-2016-10518 (Remote Memory Disclosure in ws); CVE-2024-37890 (ws affected by a DoS when handling a request with many HTTP headers); CVE-2026-45736 (ws: Uninitialized memory disclosure) +4 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
88% confidence
Finding
The manifest references ws without pinning an exact version, and the package family has multiple published advisories, so there is no reliable way to determine from this file whether installs will avoid affected releases. In a network-facing Discord voice plugin, an unresolved advisory state on a WebSocket stack can expose the service to denial of service or information disclosure depending on the resolved version.

Unpinned Dependencies

Low
Category
Supply Chain
Content
]
  },
  "devDependencies": {
    "@sinclair/typebox": "^0.34.48",
    "@types/node": "^25.0.10",
    "@types/ws": "^8.18.0",
    "typescript": "^5.9.3"
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
src/streaming-tts.ts:22

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
src/stt.ts:22

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
src/tts.ts:23

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
src/streaming-tts.ts:37