Skip to content

Commit 5a421d4

Browse files
authored
fix(cloudfront-signer): do not use URL Object (#7237)
1 parent b94ebff commit 5a421d4

File tree

2 files changed

+67
-9
lines changed

2 files changed

+67
-9
lines changed

packages/cloudfront-signer/src/sign.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,21 @@ function createSignature(data: string): string {
4646
signer.update(data);
4747
return normalizeBase64(signer.sign(privateKey, "base64"));
4848
}
49+
4950
function verifySignature(signature: string, data: string): boolean {
5051
const verifier = createVerify("RSA-SHA1");
5152
verifier.update(data);
5253
return verifier.verify(privateKey, signature, "base64");
5354
}
55+
5456
function encodeToBase64(str: string): string {
5557
return normalizeBase64(Buffer.from(str).toString("base64"));
5658
}
59+
5760
function normalizeBase64(str: string): string {
5861
return str.replace(/\+/g, "-").replace(/=/g, "_").replace(/\//g, "~");
5962
}
63+
6064
function denormalizeBase64(str: string): string {
6165
return str.replace(/\-/g, "+").replace(/_/g, "=").replace(/~/g, "/");
6266
}
@@ -78,6 +82,7 @@ describe("getSignedUrl", () => {
7882
}
7983
expect(result.query["foo"]).toBe("bar &=; baz");
8084
});
85+
8186
it("should include url path in policy of signed URL", () => {
8287
const url = "https://example.com/private.jpeg?foo=bar";
8388
const result = parseUrl(
@@ -108,6 +113,7 @@ describe("getSignedUrl", () => {
108113
});
109114
expect(verifySignature(signatureQueryParam, policyStr)).toBeTruthy();
110115
});
116+
111117
it("should sign a URL with a canned policy", () => {
112118
const result = getSignedUrl({
113119
url,
@@ -135,6 +141,7 @@ describe("getSignedUrl", () => {
135141
const signatureQueryParam = denormalizeBase64(parsedUrl.query!["Signature"] as string);
136142
expect(verifySignature(signatureQueryParam, policyStr)).toBeTruthy();
137143
});
144+
138145
it("should sign a URL with a custom policy containing a start date", () => {
139146
const result = getSignedUrl({
140147
url,
@@ -166,6 +173,7 @@ describe("getSignedUrl", () => {
166173
const signatureQueryParam = denormalizeBase64(parsedUrl.query!["Signature"] as string);
167174
expect(verifySignature(signatureQueryParam, policyStr)).toBeTruthy();
168175
});
176+
169177
it("should sign a URL with a custom policy containing an ip address", () => {
170178
const result = getSignedUrl({
171179
url,
@@ -197,6 +205,7 @@ describe("getSignedUrl", () => {
197205
const signatureQueryParam = denormalizeBase64(parsedUrl.query!["Signature"] as string);
198206
expect(verifySignature(signatureQueryParam, policyStr)).toBeTruthy();
199207
});
208+
200209
it("should sign a URL with a custom policy containing a start date and ip address", () => {
201210
const result = getSignedUrl({
202211
url,
@@ -232,6 +241,7 @@ describe("getSignedUrl", () => {
232241
const signatureQueryParam = denormalizeBase64(parsedUrl.query!["Signature"] as string);
233242
expect(verifySignature(signatureQueryParam, policyStr)).toBeTruthy();
234243
});
244+
235245
it("should allow an ip address with and without a mask", () => {
236246
const baseArgs = {
237247
url,
@@ -253,6 +263,7 @@ describe("getSignedUrl", () => {
253263
})
254264
).toBeTruthy();
255265
});
266+
256267
it("should throw an error when the ip address is invalid", () => {
257268
const baseArgs = {
258269
url,
@@ -298,6 +309,7 @@ describe("getSignedUrl", () => {
298309
})
299310
).toThrow('IP address "10.0.0.256" is invalid due to invalid IP octets.');
300311
});
312+
301313
it("should sign a RTMP URL", () => {
302314
const url = "rtmp://d111111abcdef8.cloudfront.net/private-content/private.jpeg";
303315
const result = getSignedUrl({
@@ -325,6 +337,7 @@ describe("getSignedUrl", () => {
325337
);
326338
expect(verifySignature(denormalizeBase64(signature), policyStr)).toBeTruthy();
327339
});
340+
328341
it("should sign a URL with a policy provided by the user", () => {
329342
const policy = '{"foo":"bar"}';
330343
const result = getSignedUrl({
@@ -339,6 +352,7 @@ describe("getSignedUrl", () => {
339352
const signatureQueryParam = denormalizeBase64(signature);
340353
expect(verifySignature(signatureQueryParam, policy)).toBeTruthy();
341354
});
355+
342356
it("should sign a URL automatically extracted from a policy provided by the user", () => {
343357
const policy = JSON.stringify({ Statement: [{ Resource: url }] });
344358
const result = getSignedUrl({
@@ -352,6 +366,23 @@ describe("getSignedUrl", () => {
352366
const signatureQueryParam = denormalizeBase64(signature);
353367
expect(verifySignature(signatureQueryParam, policy)).toBeTruthy();
354368
});
369+
370+
describe("should not normalize the URL", () => {
371+
it.each([".", ".."])("with '%s'", (folderName) => {
372+
const urlWithFolderName = `https://d111111abcdef8.cloudfront.net/public-content/${folderName}/private-content/private.jpeg`;
373+
const policy = JSON.stringify({ Statement: [{ Resource: urlWithFolderName }] });
374+
const result = getSignedUrl({
375+
keyPairId,
376+
privateKey,
377+
policy,
378+
passphrase,
379+
});
380+
const signature = createSignature(policy);
381+
expect(result.startsWith(urlWithFolderName)).toBeTruthy();
382+
const signatureQueryParam = denormalizeBase64(signature);
383+
expect(verifySignature(signatureQueryParam, policy)).toBeTruthy();
384+
});
385+
});
355386
});
356387

357388
describe("getSignedCookies", () => {
@@ -376,6 +407,7 @@ describe("getSignedCookies", () => {
376407
})
377408
).toBeTruthy();
378409
});
410+
379411
it("should throw an error when the ip address is invalid", () => {
380412
const baseArgs = {
381413
url,
@@ -421,6 +453,7 @@ describe("getSignedCookies", () => {
421453
})
422454
).toThrow('IP address "10.0.0.256" is invalid due to invalid IP octets.');
423455
});
456+
424457
it("should be able sign cookies that contain a URL with wildcards", () => {
425458
const url = "https://example.com/private-content/*";
426459
const result = getSignedCookies({
@@ -444,6 +477,7 @@ describe("getSignedCookies", () => {
444477
});
445478
expect(verifySignature(denormalizeBase64(result["CloudFront-Signature"]), policyStr)).toBeTruthy();
446479
});
480+
447481
it("should sign cookies with a canned policy", () => {
448482
const result = getSignedCookies({
449483
url,
@@ -475,6 +509,7 @@ describe("getSignedCookies", () => {
475509
expect(result["CloudFront-Signature"]).toBe(expected["CloudFront-Signature"]);
476510
expect(verifySignature(denormalizeBase64(result["CloudFront-Signature"]), policyStr)).toBeTruthy();
477511
});
512+
478513
it("should sign cookies with a custom policy containing a start date", () => {
479514
const result = getSignedCookies({
480515
url,
@@ -510,6 +545,7 @@ describe("getSignedCookies", () => {
510545
expect(result["CloudFront-Signature"]).toBe(expected["CloudFront-Signature"]);
511546
expect(verifySignature(denormalizeBase64(result["CloudFront-Signature"]), policyStr)).toBeTruthy();
512547
});
548+
513549
it("should sign cookies with a custom policy containing an ip address", () => {
514550
const result = getSignedCookies({
515551
url,
@@ -545,6 +581,7 @@ describe("getSignedCookies", () => {
545581
expect(result["CloudFront-Signature"]).toBe(expected["CloudFront-Signature"]);
546582
expect(verifySignature(denormalizeBase64(result["CloudFront-Signature"]), policyStr)).toBeTruthy();
547583
});
584+
548585
it("should sign cookies with a custom policy containing a start date and ip address", () => {
549586
const result = getSignedCookies({
550587
url,
@@ -584,6 +621,7 @@ describe("getSignedCookies", () => {
584621
expect(result["CloudFront-Signature"]).toBe(expected["CloudFront-Signature"]);
585622
expect(verifySignature(denormalizeBase64(result["CloudFront-Signature"]), policyStr)).toBeTruthy();
586623
});
624+
587625
it("should sign cookies with a policy provided by the user without a url", () => {
588626
const policy = '{"foo":"bar"}';
589627
const result = getSignedCookies({
@@ -612,6 +650,7 @@ describe("getSignedUrl- when signing a URL with a date range", () => {
612650
const dateGreaterThanNumber = 1716034245000;
613651
const dateObject = new Date(dateString);
614652
const dateGreaterThanObject = new Date(dateGreaterThanString);
653+
615654
it("allows string input compatible with Date constructor", () => {
616655
const epochDateLessThan = Math.round(new Date(dateString).getTime() / 1000);
617656
const resultUrl = getSignedUrl({
@@ -653,6 +692,7 @@ describe("getSignedUrl- when signing a URL with a date range", () => {
653692
expect(resultUrl).toContain(`Expires=${epochDateLessThan}`);
654693
expect(resultCookies["CloudFront-Expires"]).toBe(epochDateLessThan);
655694
});
695+
656696
it("allows Date object input", () => {
657697
const epochDateLessThan = Math.round(dateObject.getTime() / 1000);
658698
const resultUrl = getSignedUrl({
@@ -673,6 +713,7 @@ describe("getSignedUrl- when signing a URL with a date range", () => {
673713
expect(resultUrl).toContain(`Expires=${epochDateLessThan}`);
674714
expect(resultCookies["CloudFront-Expires"]).toBe(epochDateLessThan);
675715
});
716+
676717
it("allows string input for date range", () => {
677718
const result = getSignedUrl({
678719
url,
@@ -736,6 +777,7 @@ describe("getSignedUrl- when signing a URL with a date range", () => {
736777
const signatureQueryParam = denormalizeBase64(parsedUrl.query!["Signature"] as string);
737778
expect(verifySignature(signatureQueryParam, policyStr)).toBeTruthy();
738779
});
780+
739781
it("allows Date object input for date range", () => {
740782
const result = getSignedUrl({
741783
url,

packages/cloudfront-signer/src/sign.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ export type CloudfrontSignInput = CloudfrontSignInputWithParameters | Cloudfront
1212
export type CloudfrontSignerCredentials = {
1313
/** The ID of the Cloudfront key pair. */
1414
keyPairId: string;
15+
1516
/** The content of the Cloudfront private key. */
1617
privateKey: string | Buffer;
18+
1719
/** The passphrase of RSA-SHA1 key*/
1820
passphrase?: string;
1921
};
@@ -24,12 +26,16 @@ export type CloudfrontSignerCredentials = {
2426
export type CloudfrontSignInputWithParameters = CloudfrontSignerCredentials & {
2527
/** The URL string to sign. */
2628
url: string;
29+
2730
/** The date string for when the signed URL or cookie can no longer be accessed */
2831
dateLessThan: string | number | Date;
32+
2933
/** The date string for when the signed URL or cookie can start to be accessed. */
3034
dateGreaterThan?: string | number | Date;
35+
3136
/** The IP address string to restrict signed URL access to. */
3237
ipAddress?: string;
38+
3339
/**
3440
* [policy] should not be provided when using separate
3541
* dateLessThan, dateGreaterThan, or ipAddress inputs.
@@ -50,12 +56,16 @@ export type CloudfrontSignInputWithPolicy = CloudfrontSignerCredentials & {
5056
* This will be ignored if calling getSignedCookies with a policy.
5157
*/
5258
url?: string;
59+
5360
/** The JSON-encoded policy string */
5461
policy: string;
62+
5563
/** When using a policy, a separate dateLessThan should not be provided. */
5664
dateLessThan?: never;
65+
5766
/** When using a policy, a separate dateGreaterThan should not be provided. */
5867
dateGreaterThan?: never;
68+
5969
/** When using a policy, a separate ipAddress should not be provided. */
6070
ipAddress?: never;
6171
};
@@ -66,10 +76,13 @@ export type CloudfrontSignInputWithPolicy = CloudfrontSignerCredentials & {
6676
export interface CloudfrontSignedCookiesOutput {
6777
/** ID of the Cloudfront key pair. */
6878
"CloudFront-Key-Pair-Id": string;
79+
6980
/** Hashed, signed, and base64-encoded version of the JSON policy. */
7081
"CloudFront-Signature": string;
82+
7183
/** The unix date time for when the signed URL or cookie can no longer be accessed. */
7284
"CloudFront-Expires"?: number;
85+
7386
/** Base64-encoded version of the JSON policy. */
7487
"CloudFront-Policy"?: string;
7588
}
@@ -123,14 +136,14 @@ export function getSignedUrl({
123136
baseUrl = resources[0].replace("*://", "https://");
124137
}
125138

126-
const newURL = new URL(baseUrl!);
127-
newURL.search = Array.from(newURL.searchParams.entries())
128-
.concat(Object.entries(cloudfrontSignBuilder.createCloudfrontAttribute()))
139+
const startFlag = baseUrl!.includes("?") ? "&" : "?";
140+
const params = Object.entries(cloudfrontSignBuilder.createCloudfrontAttribute())
129141
.filter(([, value]) => value !== undefined)
130142
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
131143
.join("&");
144+
const urlString = baseUrl + startFlag + params;
132145

133-
return getResource(newURL);
146+
return getResource(urlString);
134147
}
135148

136149
/**
@@ -236,15 +249,18 @@ function getPolicyResources(policy: string | Policy) {
236249
/**
237250
* @internal
238251
*/
239-
function getResource(url: URL): string {
240-
switch (url.protocol) {
252+
function getResource(urlString: string): string {
253+
const protocol = urlString.slice(0, urlString.indexOf("//"));
254+
switch (protocol) {
241255
case "http:":
242256
case "https:":
243257
case "ws:":
244258
case "wss:":
245-
return url.toString();
259+
return urlString;
246260
case "rtmp:":
247-
return url.pathname.replace(/^\//, "") + url.search + url.hash;
261+
const url = new URL(urlString);
262+
const origin = `${protocol}//${url.hostname}`;
263+
return urlString.substring(origin.length).replace(/(?::\d+)?\//, "");
248264
default:
249265
throw new Error("Invalid URI scheme. Scheme must be one of http, https, or rtmp");
250266
}
@@ -407,7 +423,7 @@ class CloudfrontSignBuilder {
407423
if (!url || !dateLessThan) {
408424
return false;
409425
}
410-
const resource = getResource(new URL(url));
426+
const resource = getResource(url);
411427
const parsedDates = this.parseDateWindow(dateLessThan, dateGreaterThan);
412428
this.dateLessThan = parsedDates.dateLessThan;
413429
this.customPolicy = Boolean(parsedDates.dateGreaterThan) || Boolean(ipAddress);

0 commit comments

Comments
 (0)