-
-
Notifications
You must be signed in to change notification settings - Fork 39.6k
Description
CVSS Assessment
| Metric | Value |
|---|---|
| Score | 5.4 / 10.0 |
| Severity | Medium |
| Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N |
Summary
The BlueBubbles extension constructs multipart/form-data requests with user-controlled filenames that are insufficiently sanitized. The sanitizeFilename function uses path.basename() which removes directory components but does not escape special characters like double quotes ("), carriage returns (\r), or line feeds (\n). This allows an attacker who can control the filename to inject additional multipart headers or fields.
Additionally, the setGroupIconBlueBubbles function has no filename sanitization at all, making it equally vulnerable.
Affected Code
File: extensions/bluebubbles/src/attachments.ts:26-30
function sanitizeFilename(input: string | undefined, fallback: string): string {
const trimmed = input?.trim() ?? "";
const base = trimmed ? path.basename(trimmed) : "";
return base || fallback;
}File: extensions/bluebubbles/src/attachments.ts:224-228
const addFile = (name: string, fileBuffer: Uint8Array, fileName: string, mimeType?: string) => {
parts.push(encoder.encode(`--${boundary}\r\n`));
parts.push(
encoder.encode(`Content-Disposition: form-data; name="${name}"; filename="${fileName}"\r\n`),
);
// ...
};File: extensions/bluebubbles/src/chat.ts:313-342 (additional instance)
export async function setGroupIconBlueBubbles(
chatGuid: string,
buffer: Uint8Array,
filename: string, // No sanitization applied
opts: BlueBubblesChatOpts & { contentType?: string } = {},
): Promise<void> {
// ...
parts.push(
encoder.encode(`Content-Disposition: form-data; name="icon"; filename="${filename}"\r\n`),
);
// ...
}Attack Surface
How is this reached?
- Network / [ ] Adjacent / [ ] Local / [ ] Physical
Authentication required?
- None / [x] Low / [ ] High
Entry points:
- The
sendBlueBubblesAttachmentfunction accepts afilenameparameter that flows through insufficient sanitization to the multipart Content-Disposition header. - The
setGroupIconBlueBubblesfunction accepts afilenameparameter with no sanitization at all, passed directly to Content-Disposition header.
Both are called from actions.ts with user-controlled input from readStringParam(params, "filename").
Exploit Conditions
Complexity: [x] Low / [ ] High
User interaction: [x] None / [ ] Required
Prerequisites: Attacker must be able to send messages through an OpenClaw instance with BlueBubbles configured, and control the filename of an attachment or group icon.
Impact Assessment
Scope: [x] Unchanged / [ ] Changed
| Impact Type | Level | Description |
|---|---|---|
| Confidentiality | Low | Could potentially inject form fields to extract data from BlueBubbles server responses |
| Integrity | Low | Could inject additional form fields to modify the request to BlueBubbles server |
| Availability | None | No direct availability impact |
Steps to Reproduce
- Configure OpenClaw with BlueBubbles integration
- Craft a filename containing injection payload, for example:
test.jpg"\r\nContent-Disposition: form-data; name="method"\r\n\r\nprivate-api-injected\r\n-- - Send an attachment with this filename through the BlueBubbles channel (via
sendAttachmentaction) or set a group icon (viasetGroupIconaction) - The multipart form-data body will contain the injected headers
Example malformed request body:
------BlueBubblesFormBoundary...
Content-Disposition: form-data; name="attachment"; filename="test.jpg"
Content-Disposition: form-data; name="method"
private-api-injected
--"
Content-Type: image/jpeg
[binary data]
------BlueBubblesFormBoundary...
Recommended Fix
Replace sanitizeFilename with a function that properly escapes or removes dangerous characters, and apply it to both affected locations:
function sanitizeFilename(input: string | undefined, fallback: string): string {
const trimmed = input?.trim() ?? "";
const base = trimmed ? path.basename(trimmed) : "";
if (!base) return fallback;
// Remove or escape characters that could break Content-Disposition
// RFC 5987 / RFC 6266 compliant approach:
// 1. Remove control characters (0x00-0x1F, 0x7F)
// 2. Escape or remove double quotes
// 3. Remove backslashes or escape them
return base
.replace(/[\x00-\x1F\x7F]/g, "") // Remove control characters (includes \r, \n)
.replace(/"/g, "'") // Replace double quotes with single quotes
.replace(/\\/g, "_") // Replace backslashes with underscore
.slice(0, 255); // Limit length
}Apply this sanitization in setGroupIconBlueBubbles (chat.ts:313) as well, since it currently has no sanitization:
export async function setGroupIconBlueBubbles(
chatGuid: string,
buffer: Uint8Array,
filename: string,
opts: BlueBubblesChatOpts & { contentType?: string } = {},
): Promise<void> {
const sanitizedFilename = sanitizeFilename(filename, "icon.png"); // Add this
// ... use sanitizedFilename instead of filename in Content-Disposition
}Alternatively, use percent-encoding for the filename parameter as per RFC 5987:
function encodeFilenameRFC5987(filename: string): string {
return "utf-8''" + encodeURIComponent(filename).replace(/'/g, "%27");
}
// Usage in Content-Disposition:
// Content-Disposition: form-data; name="attachment"; filename*=${encodeFilenameRFC5987(fileName)}References
- CWE: CWE-93 - Improper Neutralization of CRLF Sequences ('CRLF Injection')
- RFC 6266: Use of the Content-Disposition Header Field in HTTP
- RFC 5987: Character Set and Language Encoding for HTTP Header Field Parameters