can1357/oh-my-pi · error · Error

Shared-folder destination escapes its configured root

Error message

Shared-folder destination escapes its configured root

What it means

Thrown by the shared-folder uploader after resolving the final target path when it falls outside the configured root directory. This is a path-traversal guard: path.resolve collapses any ../ segments, and the resolved absolute target must equal the root or live beneath root + path separator, otherwise the upload is refused.

Source

Thrown at packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts:281

				throw error;
			}
			return publication("ftp", request, publicUrl(publicBase, directory, filename));
		},
	};
}

function createSharedFolderUploader(config: DestinationRuntimeConfig): BlobUploader {
	const root = path.resolve(requiredStringOption(config, "root"));
	const directory = optionString(config, "path");
	const publicBase = requiredStringOption(config, "publicBaseUrl");
	httpBase(publicBase, "publicBaseUrl");
	return {
		destination: "shared-folder",
		async upload(request) {
			const filename = safeFileName(request);
			const target = path.resolve(root, ...pathParts(directory), filename);
			if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
				throw new Error("Shared-folder destination escapes its configured root");
			}
			await Bun.write(target, request.bytes, { createPath: true });
			return publication("shared-folder", request, publicUrl(publicBase, directory, filename));
		},
	};
}

function createOwnCloudUploader(config: DestinationRuntimeConfig): BlobUploader {
	const host = httpBase(requiredStringOption(config, "host"), "host").toString().replace(/\/$/, "");
	const username = requireCredential(config, "username");
	const password = requireCredential(config, "password");
	const directory = optionString(config, "path");
	const direct = optionBoolean(config, "directLink", true) ?? true;
	const preview = optionBoolean(config, "previewLink", false) ?? false;
	const expiryDays = optionNumber(config, "expiryDays");
	const authorization = basicAuthorization(username, password);
	const requestHeaders = { Authorization: authorization, "OCS-APIREQUEST": "true" };
	const requestFetch = fetchFor(config);

View on GitHub (pinned to 9690622007)

Solutions

  1. Inspect the resolved target vs configured root in the log; ensure options.path contains no traversal segments.
  2. Remove or replace symlinks under the shared-folder root that point outside it, or point root at the real directory.
  3. Set options.root to a canonical absolute path (no trailing separator, fully resolved) so the startsWith check matches.
  4. If you genuinely need writes outside root, configure a second destination whose root is that directory instead of bypassing the guard.

Example fix

// before
{ "options": { "root": "/srv/share", "path": "../other" } }
// after
{ "options": { "root": "/srv/share", "path": "uploads" } }
Defensive patterns

Strategy: validation

Validate before calling

import * as path from 'node:path';
const root = path.resolve(dest.options.root);
const target = path.resolve(root, dest.options.path ?? '.', safeFileName);
if (target !== root && !target.startsWith(root + path.sep)) throw new Error('resolved upload path escapes shared-folder root');

Try / catch

try {
  await uploader.upload(request);
} catch (err) {
  if (err instanceof Error && err.message.includes('escapes its configured root')) {
    // reject the request as a security violation; audit the supplied path/filename
  } else throw err;
}

Prevention

When it happens

Trigger: options.path (directory) containing segments that escape root — e.g. path "../../etc" — or a filename crafted to traverse after pathParts() normalization; note pathParts already rejects ".." segments and NUL bytes, so this usually fires when root itself is symlinked/renamed or resolve produces an unexpected location.

Common situations: Symlinked subdirectory inside root pointing outside; root configured as a relative path that resolves differently than expected; hostile filenames in multi-tenant setups; mistyped options.path like "/srv/share/../../var/www".

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/ecc53498948231ae. Report an issue: GitHub.