Skip to content

[Bug]: BlueBubbles attachment filename allows multipart header injection #10333

@coygeek

Description

@coygeek

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

CVSS v3.1 Calculator

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:

  1. The sendBlueBubblesAttachment function accepts a filename parameter that flows through insufficient sanitization to the multipart Content-Disposition header.
  2. The setGroupIconBlueBubbles function accepts a filename parameter 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

  1. Configure OpenClaw with BlueBubbles integration
  2. 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--
    
  3. Send an attachment with this filename through the BlueBubbles channel (via sendAttachment action) or set a group icon (via setGroupIcon action)
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions