Skip to content

Commit 37c5c2d

Browse files
committed
security: detect encoded path traversal specifiers
1 parent 586a45d commit 37c5c2d

2 files changed

Lines changed: 62 additions & 1 deletion

File tree

packages/security/src/index.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export class DefaultSecurityChecker implements SecurityChecker {
383383
const issues: string[] = [];
384384
const diagnostics: RuntimeDiagnostic[] = [];
385385

386-
if (specifier.includes("..")) {
386+
if (this.hasPathTraversalSequence(specifier)) {
387387
issues.push(
388388
`Path traversal is not allowed in module specifier: ${specifier}`,
389389
);
@@ -767,6 +767,43 @@ export class DefaultSecurityChecker implements SecurityChecker {
767767
return false;
768768
}
769769
}
770+
771+
private hasPathTraversalSequence(specifier: string): boolean {
772+
if (specifier.includes("..")) {
773+
return true;
774+
}
775+
776+
for (const decoded of this.decodeSpecifierVariants(specifier)) {
777+
if (decoded.includes("..")) {
778+
return true;
779+
}
780+
}
781+
782+
return false;
783+
}
784+
785+
private decodeSpecifierVariants(specifier: string): string[] {
786+
const variants: string[] = [];
787+
let current = specifier;
788+
789+
for (let i = 0; i < 2; i += 1) {
790+
let decoded: string;
791+
try {
792+
decoded = decodeURIComponent(current);
793+
} catch {
794+
break;
795+
}
796+
797+
if (decoded === current) {
798+
break;
799+
}
800+
801+
variants.push(decoded);
802+
current = decoded;
803+
}
804+
805+
return variants;
806+
}
770807
}
771808

772809
function clonePolicy(policy: RuntimeSecurityPolicy): RuntimeSecurityPolicy {

tests/security.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ test("security checker allows allowed JSPM module specifiers", async () => {
6969
assert.equal(moduleResult.issues.length, 0);
7070
});
7171

72+
test("security checker blocks encoded path traversal module specifiers", async () => {
73+
const checker = new DefaultSecurityChecker();
74+
checker.initialize();
75+
76+
const encodedLowerResult = checker.checkModuleSpecifier(
77+
"https://ga.jspm.io/%2e%2e/escape.js",
78+
);
79+
const encodedUpperResult = checker.checkModuleSpecifier(
80+
"https://ga.jspm.io/%2E%2E/escape.js",
81+
);
82+
const doubleEncodedResult = checker.checkModuleSpecifier(
83+
"https://ga.jspm.io/%252e%252e/escape.js",
84+
);
85+
86+
assert.equal(encodedLowerResult.safe, false);
87+
assert.equal(encodedUpperResult.safe, false);
88+
assert.equal(doubleEncodedResult.safe, false);
89+
assert.ok(
90+
encodedLowerResult.issues.some((issue) =>
91+
issue.includes("Path traversal is not allowed"),
92+
),
93+
);
94+
});
95+
7296
test("security checker blocks unsafe state action paths", async () => {
7397
const checker = new DefaultSecurityChecker();
7498
checker.initialize();

0 commit comments

Comments
 (0)