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
- Inspect the resolved target vs configured root in the log; ensure options.path contains no traversal segments.
- Remove or replace symlinks under the shared-folder root that point outside it, or point root at the real directory.
- Set options.root to a canonical absolute path (no trailing separator, fully resolved) so the startsWith check matches.
- 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
- Sanitize user-supplied paths: reject '..' segments and absolute paths before passing them as options.path.
- Resolve root to a canonical absolute path at config load and log the resolved target on upload.
- Avoid symlinks inside the shared root, or resolve and re-check the real path (fs.realpath) before writing.
- Treat this error as a security signal — audit the input that produced it.
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
- Destination paths cannot contain parent traversal or NUL byt
- Absolute paths are not allowed in ${scheme}:// URLs: ${rawPa
- Path traversal (..) is not allowed in ${scheme}:// URLs: ${r
- Unsafe #{scheme}:// path (absolute or traversal): #{path}
- Invalid path: ${localPath} resolves outside working director
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/ecc53498948231ae.
Report an issue: GitHub.