Files
openclaw/src/gateway/security-path.test.ts
Peter Steinberger 26312fd0b0 refactor: remove gateway and channel dead exports (#106038)
* refactor: remove gateway and channel dead exports

* style: format config reload test

* refactor: remove newly dead gateway exports

* test: preserve approval audience coverage

* refactor: preserve gateway contracts while pruning exports

* test: retain gateway regression coverage

* test: restore gateway policy coverage

* test: close dead-export CI gaps

* fix: reconcile dead exports with latest main

* refactor: remove newly dead gateway runner export

* test: remove private worker verifier coverage

* test: preserve weak secret docs coverage

* fix: avoid gateway health import cycle

* fix: preserve node pairing type contract

* style: format talk relay test

* fix: preserve gateway protocol generation contract

* chore: refresh dead export baseline

* fix: preserve loaded target compatibility exports

* fix: preserve plugin SDK declaration resolution

* chore: refresh dead export baseline

* chore: refresh dead export baseline

* test(gateway): derive private worker service options
2026-07-13 05:47:05 -07:00

98 lines
4.2 KiB
TypeScript

// Covers gateway protected-path canonicalization for repeated encoding,
// malformed encodings, dot segments, and plugin route prefixes.
import { describe, expect, it } from "vitest";
import { canonicalizePathForSecurity } from "./security-path.js";
import {
isProtectedPluginRoutePathFromContext,
resolvePluginRoutePathContext,
} from "./server/plugins-http/path-context.js";
function isProtectedPluginRoutePath(pathname: string): boolean {
return isProtectedPluginRoutePathFromContext(resolvePluginRoutePathContext(pathname));
}
function buildRepeatedEncodedSlashPath(depth: number): string {
let encodedSlash = "%2f";
for (let i = 1; i < depth; i++) {
encodedSlash = encodedSlash.replace(/%/g, "%25");
}
return `/api${encodedSlash}channels${encodedSlash}nostr${encodedSlash}default${encodedSlash}profile`;
}
describe("security-path canonicalization", () => {
it("canonicalizes decoded case/slash variants", () => {
expect(canonicalizePathForSecurity("/API/channels//nostr/default/profile/")).toEqual({
canonicalPath: "/api/channels/nostr/default/profile",
candidates: ["/api/channels/nostr/default/profile"],
malformedEncoding: false,
decodePasses: 0,
decodePassLimitReached: false,
rawNormalizedPath: "/api/channels/nostr/default/profile",
});
const encoded = canonicalizePathForSecurity("/api/%63hannels%2Fnostr%2Fdefault%2Fprofile");
expect(encoded.canonicalPath).toBe("/api/channels/nostr/default/profile");
expect(encoded.candidates).toContain("/api/%63hannels%2fnostr%2fdefault%2fprofile");
expect(encoded.candidates).toContain("/api/channels/nostr/default/profile");
expect(encoded.decodePasses).toBeGreaterThan(0);
expect(encoded.decodePassLimitReached).toBe(false);
});
it("resolves traversal after repeated decoding", () => {
expect(
canonicalizePathForSecurity("/api/foo/..%2fchannels/nostr/default/profile").canonicalPath,
).toBe("/api/channels/nostr/default/profile");
expect(
canonicalizePathForSecurity("/api/foo/%252e%252e%252fchannels/nostr/default/profile")
.canonicalPath,
).toBe("/api/channels/nostr/default/profile");
});
it("marks malformed encoding", () => {
expect(canonicalizePathForSecurity("/api/channels%2").malformedEncoding).toBe(true);
expect(canonicalizePathForSecurity("/api/channels%zz").malformedEncoding).toBe(true);
});
it("resolves deeply encoded path separators and protects the route", () => {
const deeplyEncoded = "/api%2525252fchannels%2525252fnostr%2525252fdefault%2525252fprofile";
const result = canonicalizePathForSecurity(deeplyEncoded);
expect(result.canonicalPath).toBe("/api/channels/nostr/default/profile");
expect(result.decodePasses).toBeGreaterThanOrEqual(4);
expect(isProtectedPluginRoutePath(deeplyEncoded)).toBe(true);
});
it("reports decode depth overflow and fails closed", () => {
const excessiveDepthPath = buildRepeatedEncodedSlashPath(40);
const result = canonicalizePathForSecurity(excessiveDepthPath);
expect(result.decodePassLimitReached).toBe(true);
expect(result.malformedEncoding).toBe(false);
expect(isProtectedPluginRoutePath(excessiveDepthPath)).toBe(true);
});
});
describe("security-path protected-prefix matching", () => {
const channelVariants = [
"/API/channels/nostr/default/profile",
"/api/channels%2Fnostr%2Fdefault%2Fprofile",
"/api/%63hannels/nostr/default/profile",
"/api/foo/..%2fchannels/nostr/default/profile",
"/api/foo/%2e%2e%2fchannels/nostr/default/profile",
"/api/foo/%252e%252e%252fchannels/nostr/default/profile",
"/api%2525252fchannels%2525252fnostr%2525252fdefault%2525252fprofile",
"/api/channels%2",
"/api/channels%zz",
];
for (const path of channelVariants) {
it(`protects plugin channel path variant: ${path}`, () => {
expect(isProtectedPluginRoutePath(path)).toBe(true);
});
}
it("does not protect unrelated paths", () => {
expect(isProtectedPluginRoutePath("/plugin/public")).toBe(false);
expect(isProtectedPluginRoutePath("/api/channels-public")).toBe(false);
expect(isProtectedPluginRoutePath("/api/foo/..%2fchannels-public")).toBe(false);
expect(isProtectedPluginRoutePath("/api/channel")).toBe(false);
});
});