T09 · Insecure Skill Coding Practices
- Location
voice-notes-pro.js:318- Finding
Message-Controlled File Path Enables Arbitrary Local File Disclosure to OpenAI
- Content
View full analysis
Vulnerability Details
File Location:
voice-notes-pro.js:58-67, 318-337
Vulnerability Type: Arbitrary local file read and external data disclosure
Risk Level: HighVulnerable Code
js async function transcribeAudio(audioPath) { try { const client = getOpenAIClient(); const audioFile = await fs.readFile(audioPath); const response = await client.audio.transcriptions.create({ file: audioFile, model: CONFIG.whisper.model, language: CONFIG.whisper.language, }); return response.text;js async execute({ message, context }) { // Check whether the message contains an audio or voice attachment if (!message.audio && !message.voice) { return { success: false, message: 'Ten skill obsuguje tylko notatki gosowe', }; } // Obtain the audio file path const audioPath = message.audio?.path || message.voice?.path; if (!audioPath) { return { success: false, message: 'Nie znaleziono pliku audio', }; } // Process the voice note const result = await processVoiceNote(audioPath);Technical Analysis
The Skill obtains
audioPathdirectly from the incomingmessage.audio.pathormessage.voice.pathproperty and passes it tofs.readFile()without applying any security validation.The implementation does not:
- Resolve and validate the canonical path.
- Restrict access to a dedicated media-upload directory.
- reject absolute paths or directory traversal.
- Reject symbolic links.
- Verify that the target is a regular file.
- Validate the file extension, MIME type, or audio signature.
- Enforce a maximum file size.
Consequently, any party capable of controlling the relevant message metadata can cause the Skill process to read an arbitrary file available under its operating-system privileges. The bytes are then supplied as the
filevalue to `client.audi ...[truncated 1997 chars]- Remediation
View remediation
Remediation Suggestions
- Do not accept a raw filesystem path from message-controlled data. Use a trusted attachment identifier or stream supplied and validated by the OpenClaw runtime.
- Define a dedicated media-upload root and resolve both the allowed root and candidate path with
fs.realpath(). Reject the request unless the candidate remains strictly inside that root. - Reject absolute paths and traversal components before resolution as defense in depth.
- Use
fs.lstat()andfs.stat()to reject symbolic links and require a regular file. - Validate an allowlist of supported audio formats using file signatures, not only extensions or client-provided MIME types.
- Enforce a conservative maximum file size before reading or uploading the file. Prefer bounded streaming where supported.
- Run the Skill under a dedicated, unprivileged operating-system account with access only to required media and note directories. Avoid running it as root.
- Return generic errors to users and avoid exposing local paths or provider details.
- Add automated tests covering absolute paths,
../traversal, symlink escapes, non-audio files, oversized files, and paths outside the approved media directory.
Example containment logic should compare canonical paths with a separator-aware prefix:
js const uploadRoot = await fs.realpath('/var/lib/openclaw/uploads'); const candidate = await fs.realpath(audioPath); if ( candidate !== uploadRoot && !candidate.startsWith(uploadRoot + path.sep) ) { throw new Error('Unauthorized attachment path'); } const stat = await fs.stat(candidate); if (!stat.isFile() || stat.size > MAX_AUDIO_BYTES) { throw new Error('Invalid audio attachment'); }
