Skip to content

Remote-host attachments sharing a sanitized name silently overwrite each other (post-sanitization basename collision) #387

Description

@postoso

Version: oracle 0.18.0 (main @ 083bba7e), remote-host path (ORACLE_REMOTE_HOST / browser.remoteHost)

Related to #385, but a distinct mechanism on a different path. #385 is the local browser upload path, where attachmentDataTransfer.ts builds each browser File from path.basename(). This one happens server-side, before the browser is involved at all, and survives any client-side guard on original basenames.

What happens

sanitizeName in src/remote/server.ts:722 maps every character outside [a-zA-Z0-9._-] to _:

function sanitizeName(raw: string): string {
  return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
}

At src/remote/server.ts:229-238 that sanitized name becomes the write path, and each attachment entry then records that path:

const safeName = sanitizeName(attachment.fileName ?? `attachment-${index + 1}`);
const filePath = path.join(attachmentDir, safeName);
await writeFile(filePath, Buffer.from(attachment.contentBase64, "base64"));
attachments.push({ path: filePath, ... });

Two attachments whose names differ only in characters the regex folds therefore resolve to the same filePath. writeFile overwrites the first payload, and both pushed entries point at the surviving file. The fallback loop at :249-258 has the same shape.

Concretely, a b.txt and a_b.txt both sanitize to a_b.txt.

Why it is easy to miss

The names are genuinely distinct on the client, so any collision check on original basenames passes. The divergence only appears after sanitization, on the server, by which point the write has already happened. There is no warning and no count mismatch, so the run reports the full number of files.

Impact

Same class as #385: silent content loss, with the surviving file wearing a plausible name. A caller sees the expected file count and a successful run while one attachment's content was never sent. The recurring-basename case that makes #385 painful in multi-directory packs applies here too, widened to any pair of names differing only in sanitized characters (spaces being the common one).

Suggested fix

Collision-free path allocation at the write site, rather than a client-side check. Two reasons to prefer the server side:

  1. A client-side guard on sanitized names requires the client to model the server's sanitization rule, which breaks the moment that regex changes.
  2. Version-mismatched clients would never receive a client-side fix, whereas server-side allocation protects every caller.

Suffixing on collision (a_b.txt, a_b-2.txt) or allocating per-attachment subdirectories both work. Erroring is also defensible for consistency with the #385 guard, though the names here are legitimately distinct from the user's point of view, which argues for allocation over rejection.

Not attempted

I did not run a live remote-host session against this; the report is from reading main at 083bba7e. The mechanism is visible in source and the two loops are unambiguous, but a live confirmation would be worth having before anyone builds on it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Urgent regression or broken agent/channel workflow affecting real users now.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:data-lossThis issue is about lost, corrupted, or silently dropped user/session/config data.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions