Filenames
sanitizeUntrustedFileName(name, fallback) reduces a filename string from an untrusted source to one traversal-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/advanced";
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 non-portable characters. C0/C1 controls (
0x00–0x1f,0x7f–0x9f) and the Windows-invalid set< > : " / \\ | ? *are removed on every platform. - Trim again.
- If the result is empty,
".", or"..", returnfallbackName. - Suffix Windows reserved basenames. Compare the part before the first
.case-insensitively with the Windows device-name set, includingCON,PRN,AUX,NUL,CLOCK$,CONIN$,CONOUT$,COM1..9,LPT1..9, and their superscript¹,², and³variants. Windows-ignored spaces and dots at the end of that basename do not disguise a device name. A match gains_before its extension, preserving the original case and extension on every platform. - Truncate. If the cleaned segment is longer than 200 UTF-16 code units, take up to the first 200 without splitting a valid Unicode surrogate pair.
That's it. The function stays intentionally small: it removes traversal and the most obvious cross-platform device and character hazards, but it is not a complete portable-filename or uniqueness policy.
#Examples
sanitizeUntrustedFileName("notes.txt", "untitled"); // "notes.txt"
sanitizeUntrustedFileName("../../etc/passwd", "upload"); // "passwd"
sanitizeUntrustedFileName("foo\\bar.png", "upload"); // "bar.png"
sanitizeUntrustedFileName("a\u0000b\tc", "upload"); // "abc"
sanitizeUntrustedFileName(" ", "fallback"); // "fallback"
sanitizeUntrustedFileName(".", "fallback"); // "fallback"
sanitizeUntrustedFileName("..", "fallback"); // "fallback"
sanitizeUntrustedFileName("CON", "fallback"); // "CON_"
sanitizeUntrustedFileName("nul.txt", "fallback"); // "nul_.txt"
sanitizeUntrustedFileName("aux.c", "fallback"); // "aux_.c"
sanitizeUntrustedFileName("conin$", "fallback"); // "conin$_"
sanitizeUntrustedFileName("a".repeat(300), "x"); // 200-char "aaa..."
#What it does not do
The function is deliberately narrow. It will not:
- Replace leading dots (so a name like
.configstays hidden on POSIX systems). - Trim trailing dots. Surrounding spaces are removed by the documented trim
- 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.
steps, but Windows-normalized dot/space spellings can still alias; reject or rewrite them when Windows portability or cross-platform migration matters.
Windows reserved basenames are handled by the default portability pass; callers no longer need to layer a separate reserved-name recipe on top.
#Common patterns
#Make a unique filename
import { sanitizeUntrustedFileName } from "@openclaw/fs-safe/advanced";
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.