actualbudget/actual · error

Invalid upload filename

Error message

Invalid upload filename

What it means

uploadFileWeb sanitizes an uploaded filename before writing it into the browser virtual /uploads directory. If after stripping path components and NUL bytes the name is empty, '.', or '..', the write is refused with 'Invalid upload filename' to prevent path traversal or writing to the directory itself.

Source

Thrown at packages/loot-core/src/server/budgetfiles/app.ts:685

  app.events.emit('load-budget', { id });

  return {};
}

async function uploadFileWeb({
  filename,
  contents,
}: {
  filename: string;
  contents: ArrayBuffer;
}) {
  if (!Platform.isBrowser) {
    return null;
  }

  const safeName = filename.split(/[/\\]/).pop()?.replaceAll('\0', '');
  if (!safeName || safeName === '.' || safeName === '..') {
    throw new Error('Invalid upload filename');
  }
  await fs.writeFile(fs.join('/uploads', safeName), contents);
  return {};
}

async function getBackups({ id }) {
  return getAvailableBackups(id);
}

async function loadBackup({ id, backupId }) {
  await _loadBackup(id, backupId);
}

async function makeBackup({ id }) {
  await _makeBackup(id);
}

async function getLastOpenedBackup() {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Check the uploaded file's name in the browser before calling the handler and ensure it is a non-empty basename like 'report.xlsx'.
  2. Strip directory components yourself with filename.split(/[/\\]/).pop() and validate the result is not '.', '..', or empty.
  3. If a drag-and-drop dropped a directory, use a file input (accepting files only) or enumerate entry.files instead of passing the directory name.
  4. Sanitize with NUL-byte removal and reject reserved names before invoking uploadFileWeb.

Example fix

// before
await send('upload-file', { filename: dirEntry.name, contents });
// after
const safeName = dirEntry.name.split(/[/\\]/).pop()?.replaceAll('\0', '');
if (safeName && safeName !== '.' && safeName !== '..') {
  await send('upload-file', { filename: safeName, contents });
}
Defensive patterns

Strategy: validation

Validate before calling

function isValidUploadName(name) {
  const safe = String(name).split(/[/\\]/).pop()?.replaceAll('\0', '');
  return Boolean(safe) && safe !== '.' && safe !== '..';
}
if (!isValidUploadName(file.name)) throw new Error('Pick a real file, not a folder');

Type guard

function hasSafeName(f: unknown): f is string {
  return typeof f === 'string' && f.length > 0 && !['.', '..'].includes(f.replaceAll('\0', ''));
}

Try / catch

try {
  await send('upload-file', { filename: file.name, contents });
} catch (e) {
  if (e.message === 'Invalid upload filename') {
    alert('The selected file has an invalid name. Choose a regular file.');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling uploadFileWeb (via the upload IPC handler from a browser file picker) with a filename that is empty, just '.', or just '..' — e.g. passing a raw directory path whose basename collapses, or a name consisting only of separators like '/' or '\\'.

Common situations: Users drag-and-drop a folder instead of a file; an integration passes an absolute path and takes the wrong basename; a buggy client sends a zero-length name field; Windows/Unix separator mixups leave only slashes after split.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/0982b06ed069e61c. Report an issue: GitHub.