Filenames
sanitizeUntrustedFileName(name, fallback) reduces a filename string from an untrusted source to a single, control-character-free path segment. Use it as a thin first pass before storing user-supplied names; pair with safeDirName when you need stricter directory-name handling.
import { sanitizeUntrustedFileName } from "@openclaw/fs-safe";
const safe = sanitizeUntrustedFileName(req.body.fileName, "upload");
await fs.write(`uploads/${safe}`, body);
#Signature
function sanitizeUntrustedFileName(fileName: string, fallbackName: string): string;
#What it does
In order:
- Trim whitespace. If the result is empty, return
fallbackName. - Strip path components. Apply
path.posix.basenamethenpath.win32.basenameso neitherfoo/bar.txtnorfoo\bar.txtsurvives — only the final segment remains. - Strip control characters. Anything below
0x20(NUL, newlines, tabs, ESC, …) and0x7f(DEL) is removed. - Trim again.
- If the result is empty,
".", or"..", returnfallbackName. - Truncate. If the cleaned segment is longer than 200 characters, take the first 200.
That's it. The function is intentionally minimal — it gives you a name that won't traverse and won't carry control bytes, without trying to second-guess what your destination filesystem accepts.
#Examples
sanitizeUntrustedFileName("notes.txt", "untitled"); // "notes.txt"
sanitizeUntrustedFileName("../../etc/passwd", "upload"); // "passwd"
sanitizeUntrustedFileName("foo\\bar.png", "upload"); // "bar.png"
sanitizeUntrustedFileName("a b c", "upload"); // "abc"
sanitizeUntrustedFileName(" ", "fallback"); // "fallback"
sanitizeUntrustedFileName(".", "fallback"); // "fallback"
sanitizeUntrustedFileName("..", "fallback"); // "fallback"
sanitizeUntrustedFileName("a".repeat(300), "x"); // 200-char "aaa..."
#What it does not do
The function is deliberately narrow. It will not:
- Replace special characters like
: * ? " < > |(Windows-reserved). On Windows-only deployments, do an extra pass yourself. - Reject Windows reserved names (
CON,PRN,AUX,NUL,COM1..9,LPT1..9). - Replace leading dots (so a name like
.configstays hidden on POSIX systems). - Trim trailing dots or spaces (Windows tolerates them silently).
- Add an extension or change case.
- Validate file content. To enforce an extension allow-list, check after sanitization.
- Deduplicate against existing files. Append a random suffix if you need uniqueness.
If your domain needs stricter handling, layer it on top:
const trimmed = sanitizeUntrustedFileName(input, "upload");
const noWindowsReserved = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..+)?$/i.test(trimmed)
? "upload"
: trimmed;
const stripWindowsSpecial = noWindowsReserved.replace(/[<>:"|?*]/g, "_");
#Common patterns
#Make a unique filename
import { sanitizeUntrustedFileName } from "@openclaw/fs-safe";
import { randomUUID } from "node:crypto";
const base = sanitizeUntrustedFileName(req.body.fileName, "upload");
const unique = `${randomUUID()}-${base}`;
await fs.write(`uploads/${unique}`, body);
#Restrict to a known set of extensions
const safe = sanitizeUntrustedFileName(req.body.fileName, "upload");
const ext = path.extname(safe).toLowerCase();
if (![".png", ".jpg", ".webp"].includes(ext)) return reply(400, "unsupported extension");
#Sanitize, then write through a Root
const safe = sanitizeUntrustedFileName(req.body.fileName, "upload");
await fs.write(`uploads/${safe}`, body); // fs is a Root() handle; rejects traversal too
#See also
- Install path helpers —
safeDirName,safePathSegmentHashedfor directory-segment sanitization. root()— the boundary you'll write into after sanitizing.