import { describe, expect, it } from "vitest" ;
import {
PROTECTED_PLUGIN_ROUTE_PREFIXES,
buildCanonicalPathCandidates,
canonicalizePathForSecurity,
isPathProtectedByPrefixes,
isProtectedPluginRoutePath,
} from "./security-path.js" ;
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(
expect.objectContaining({
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 4x encoded slash path variants to protected channel routes" , () => {
const deeplyEncoded = "/api%2525252fchannels%2525252fnostr%2525252fdefault%2525252fprofile" ;
const canonical = canonicalizePathForSecurity(deeplyEncoded);
expect(canonical.canonicalPath).toBe("/api/channels/nostr/default/profile" );
expect(canonical.decodePasses).toBeGreaterThanOrEqual(4 );
expect(isProtectedPluginRoutePath(deeplyEncoded)).toBe(true );
});
it("flags decode depth overflow and fails closed for protected prefix checks" , () => {
const excessiveDepthPath = buildRepeatedEncodedSlashPath(40 );
const candidates = buildCanonicalPathCandidates(excessiveDepthPath, 32 );
expect(candidates.decodePassLimitReached).toBe(true );
expect(candidates.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 );
expect(isPathProtectedByPrefixes(path, PROTECTED_PLUGIN_ROUTE_PREFIXES)).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 );
});
});
Messung V0.5 in Prozent C=99 H=97 G=97
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland