mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 10:21:25 +00:00
fix: harden boundary-path canonical alias handling
This commit is contained in:
@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Docker/GCP onboarding: reduce first-build OOM risk by capping Node heap during `pnpm install`, reuse existing gateway token during `docker-setup.sh` reruns so `.env` stays aligned with config, auto-bootstrap Control UI allowed origins for non-loopback Docker binds, and add GCP docs guidance for tokenized dashboard links + pairing recovery commands. (#26253) Thanks @pandego.
|
- Docker/GCP onboarding: reduce first-build OOM risk by capping Node heap during `pnpm install`, reuse existing gateway token during `docker-setup.sh` reruns so `.env` stays aligned with config, auto-bootstrap Control UI allowed origins for non-loopback Docker binds, and add GCP docs guidance for tokenized dashboard links + pairing recovery commands. (#26253) Thanks @pandego.
|
||||||
- Pairing/Multi-account isolation: keep non-default account pairing allowlists and pending requests strictly account-scoped, while default account continues to use channel-scoped pairing allowlist storage. Thanks @gumadeiras.
|
- Pairing/Multi-account isolation: keep non-default account pairing allowlists and pending requests strictly account-scoped, while default account continues to use channel-scoped pairing allowlist storage. Thanks @gumadeiras.
|
||||||
- Security/Config includes: harden `$include` file loading with verified-open reads, reject hardlinked include aliases, and enforce include file-size guardrails so config include resolution remains bounded to trusted in-root files. This ships in the next npm release (`2026.2.26`). Thanks @zpbrent for reporting.
|
- Security/Config includes: harden `$include` file loading with verified-open reads, reject hardlinked include aliases, and enforce include file-size guardrails so config include resolution remains bounded to trusted in-root files. This ships in the next npm release (`2026.2.26`). Thanks @zpbrent for reporting.
|
||||||
|
- Security/Workspace FS boundary aliases: harden canonical boundary resolution for non-existent-leaf symlink aliases while preserving valid in-root aliases, preventing first-write workspace escapes via out-of-root symlink targets. This ships in the next npm release (`2026.2.26`). Thanks @tdjackey for reporting.
|
||||||
|
|
||||||
## 2026.2.25
|
## 2026.2.25
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,37 @@ describe("resolveBoundaryPath", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows canonical aliases that still resolve inside root", async () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await withTempRoot("openclaw-boundary-path-", async (base) => {
|
||||||
|
const root = path.join(base, "workspace");
|
||||||
|
const aliasRoot = path.join(base, "workspace-alias");
|
||||||
|
const fileName = "plugin.js";
|
||||||
|
await fs.mkdir(root, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(root, fileName), "export default {}", "utf8");
|
||||||
|
await fs.symlink(root, aliasRoot);
|
||||||
|
|
||||||
|
const resolved = await resolveBoundaryPath({
|
||||||
|
absolutePath: path.join(aliasRoot, fileName),
|
||||||
|
rootPath: await fs.realpath(root),
|
||||||
|
boundaryLabel: "plugin root",
|
||||||
|
});
|
||||||
|
expect(resolved.exists).toBe(true);
|
||||||
|
expect(isPathInside(resolved.rootCanonicalPath, resolved.canonicalPath)).toBe(true);
|
||||||
|
|
||||||
|
const resolvedSync = resolveBoundaryPathSync({
|
||||||
|
absolutePath: path.join(aliasRoot, fileName),
|
||||||
|
rootPath: await fs.realpath(root),
|
||||||
|
boundaryLabel: "plugin root",
|
||||||
|
});
|
||||||
|
expect(resolvedSync.exists).toBe(true);
|
||||||
|
expect(isPathInside(resolvedSync.rootCanonicalPath, resolvedSync.canonicalPath)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("maintains containment invariant across randomized alias cases", async () => {
|
it("maintains containment invariant across randomized alias cases", async () => {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -53,8 +53,16 @@ export async function resolveBoundaryPath(
|
|||||||
? path.resolve(params.rootCanonicalPath)
|
? path.resolve(params.rootCanonicalPath)
|
||||||
: await resolvePathViaExistingAncestor(rootPath);
|
: await resolvePathViaExistingAncestor(rootPath);
|
||||||
const lexicalInside = isPathInside(rootPath, absolutePath);
|
const lexicalInside = isPathInside(rootPath, absolutePath);
|
||||||
|
const outsideLexicalCanonicalPath = lexicalInside
|
||||||
|
? undefined
|
||||||
|
: await resolvePathViaExistingAncestor(absolutePath);
|
||||||
|
const canonicalOutsideLexicalPath = outsideLexicalCanonicalPath ?? absolutePath;
|
||||||
|
|
||||||
if (!params.skipLexicalRootCheck && !lexicalInside) {
|
if (
|
||||||
|
!params.skipLexicalRootCheck &&
|
||||||
|
!lexicalInside &&
|
||||||
|
!isPathInside(rootCanonicalPath, canonicalOutsideLexicalPath)
|
||||||
|
) {
|
||||||
throw pathEscapeError({
|
throw pathEscapeError({
|
||||||
boundaryLabel: params.boundaryLabel,
|
boundaryLabel: params.boundaryLabel,
|
||||||
rootPath,
|
rootPath,
|
||||||
@@ -63,7 +71,7 @@ export async function resolveBoundaryPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!lexicalInside) {
|
if (!lexicalInside) {
|
||||||
const canonicalPath = await resolvePathViaExistingAncestor(absolutePath);
|
const canonicalPath = canonicalOutsideLexicalPath;
|
||||||
assertInsideBoundary({
|
assertInsideBoundary({
|
||||||
boundaryLabel: params.boundaryLabel,
|
boundaryLabel: params.boundaryLabel,
|
||||||
rootCanonicalPath,
|
rootCanonicalPath,
|
||||||
@@ -97,8 +105,16 @@ export function resolveBoundaryPathSync(params: ResolveBoundaryPathParams): Reso
|
|||||||
? path.resolve(params.rootCanonicalPath)
|
? path.resolve(params.rootCanonicalPath)
|
||||||
: resolvePathViaExistingAncestorSync(rootPath);
|
: resolvePathViaExistingAncestorSync(rootPath);
|
||||||
const lexicalInside = isPathInside(rootPath, absolutePath);
|
const lexicalInside = isPathInside(rootPath, absolutePath);
|
||||||
|
const outsideLexicalCanonicalPath = lexicalInside
|
||||||
|
? undefined
|
||||||
|
: resolvePathViaExistingAncestorSync(absolutePath);
|
||||||
|
const canonicalOutsideLexicalPath = outsideLexicalCanonicalPath ?? absolutePath;
|
||||||
|
|
||||||
if (!params.skipLexicalRootCheck && !lexicalInside) {
|
if (
|
||||||
|
!params.skipLexicalRootCheck &&
|
||||||
|
!lexicalInside &&
|
||||||
|
!isPathInside(rootCanonicalPath, canonicalOutsideLexicalPath)
|
||||||
|
) {
|
||||||
throw pathEscapeError({
|
throw pathEscapeError({
|
||||||
boundaryLabel: params.boundaryLabel,
|
boundaryLabel: params.boundaryLabel,
|
||||||
rootPath,
|
rootPath,
|
||||||
@@ -107,7 +123,7 @@ export function resolveBoundaryPathSync(params: ResolveBoundaryPathParams): Reso
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!lexicalInside) {
|
if (!lexicalInside) {
|
||||||
const canonicalPath = resolvePathViaExistingAncestorSync(absolutePath);
|
const canonicalPath = canonicalOutsideLexicalPath;
|
||||||
assertInsideBoundary({
|
assertInsideBoundary({
|
||||||
boundaryLabel: params.boundaryLabel,
|
boundaryLabel: params.boundaryLabel,
|
||||||
rootCanonicalPath,
|
rootCanonicalPath,
|
||||||
|
|||||||
Reference in New Issue
Block a user