Skip to content

Commit 28dcfa3

Browse files
authored
fix(middleware-ssec): ssecMiddleware with strict base64 validation (#5875)
* fix(middleware-ssec): ssecMiddleware with strict base64 validation * fix(middleware-ssec): ssecMiddleware with strict base64 validation, using serde decoder * fix(middleware-ssec): ssecMiddleware with strict base64 validation with tests --------- Co-authored-by: RanVaknin <[email protected]>
1 parent 69ecf8b commit 28dcfa3

File tree

2 files changed

+141
-37
lines changed

2 files changed

+141
-37
lines changed
+127-35
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChecksumConstructor } from "@smithy/types";
22
import * as crypto from "crypto";
33

4-
import { ssecMiddleware } from "./";
4+
import { isValidBase64EncodedSSECustomerKey, ssecMiddleware } from "./";
55

66
describe("ssecMiddleware", () => {
77
const next = jest.fn();
@@ -28,22 +28,16 @@ describe("ssecMiddleware", () => {
2828
});
2929

3030
it("should base64 encode input keys and set respective MD5 inputs", async () => {
31-
encoder1.mockReturnValue("/+JF8FMG8UVMWSaNz0s6Wg==");
32-
const key = "TestKey123";
33-
const binaryRepresentationOfKey = Buffer.from(key);
34-
const base64Key = binaryRepresentationOfKey.toString("base64");
35-
const md5Hash = crypto.createHash("md5").update(binaryRepresentationOfKey).digest();
36-
const base64Md5Hash = Buffer.from(md5Hash).toString("base64");
37-
31+
encoder2.mockReturnValue("base64");
3832
const args = {
3933
input: {
40-
SSECustomerKey: base64Key,
41-
CopySourceSSECustomerKey: base64Key,
34+
SSECustomerKey: "foo",
35+
CopySourceSSECustomerKey: "bar",
4236
},
4337
};
4438

4539
const handler = ssecMiddleware({
46-
base64Encoder: encoder1,
40+
base64Encoder: encoder2,
4741
utf8Decoder: decoder,
4842
md5: MockHash,
4943
base64Decoder: base64Decoder,
@@ -54,49 +48,147 @@ describe("ssecMiddleware", () => {
5448
expect(next.mock.calls.length).toBe(1);
5549
expect(next).toHaveBeenCalledWith({
5650
input: {
57-
SSECustomerKey: base64Key,
58-
SSECustomerKeyMD5: base64Md5Hash,
59-
CopySourceSSECustomerKey: base64Key,
60-
CopySourceSSECustomerKeyMD5: base64Md5Hash,
51+
SSECustomerKey: "base64",
52+
SSECustomerKeyMD5: "base64",
53+
CopySourceSSECustomerKey: "base64",
54+
CopySourceSSECustomerKeyMD5: "base64",
6155
},
6256
});
63-
expect(decoder.mock.calls.length).toBe(0);
64-
expect(encoder1.mock.calls.length).toBe(2);
57+
expect(decoder.mock.calls.length).toBe(2);
58+
expect(encoder2.mock.calls.length).toBe(4);
6559
expect(mockHashUpdate.mock.calls.length).toBe(2);
6660
expect(mockHashDigest.mock.calls.length).toBe(2);
67-
encoder1.mockClear();
61+
encoder2.mockClear();
6862
});
69-
it("should base64 encode input keys and set respective MD5 inputs", async () => {
70-
encoder2.mockReturnValue("base64");
63+
64+
it("should handle valid base64 encoded 32byte key input for SSECustomerKey and CopySourceSSECustomerKey", async () => {
65+
const validBase64EncodedKey = "QUIwMTIzNDU2Nzg5QUJDREVGQUJDREVGQUJDREVGQUI=";
66+
const decodedBytes = Buffer.from(validBase64EncodedKey, "base64");
67+
const md5Hash = crypto.createHash("md5").update(decodedBytes).digest();
68+
const base64EncodedMD5Hash = Buffer.from(md5Hash).toString("base64");
69+
70+
base64Decoder.mockReturnValue(decodedBytes);
71+
const mockMD5Instance = {
72+
update: jest.fn().mockReturnThis(),
73+
digest: jest.fn().mockReturnValue(md5Hash),
74+
};
75+
const mockMD5Constructor = jest.fn().mockReturnValue(mockMD5Instance);
76+
const base64Encoder = jest.fn().mockReturnValue(base64EncodedMD5Hash);
77+
78+
const handler = ssecMiddleware({
79+
base64Encoder,
80+
utf8Decoder: decoder,
81+
md5: mockMD5Constructor,
82+
base64Decoder,
83+
})(next, {} as any);
84+
7185
const args = {
7286
input: {
73-
SSECustomerKey: "foo",
74-
CopySourceSSECustomerKey: "bar",
87+
SSECustomerKey: validBase64EncodedKey,
88+
CopySourceSSECustomerKey: validBase64EncodedKey,
7589
},
7690
};
7791

92+
await handler(args);
93+
94+
expect(next).toHaveBeenCalledTimes(1);
95+
expect(next).toHaveBeenCalledWith({
96+
input: {
97+
SSECustomerKey: validBase64EncodedKey,
98+
SSECustomerKeyMD5: base64EncodedMD5Hash,
99+
CopySourceSSECustomerKey: validBase64EncodedKey,
100+
CopySourceSSECustomerKeyMD5: base64EncodedMD5Hash,
101+
},
102+
});
103+
104+
expect(base64Decoder).toHaveBeenCalledTimes(4);
105+
expect(base64Decoder).toHaveBeenCalledWith(validBase64EncodedKey);
106+
expect(mockMD5Constructor).toHaveBeenCalledTimes(2);
107+
expect(mockMD5Instance.update).toHaveBeenCalledTimes(2);
108+
expect(mockMD5Instance.update).toHaveBeenCalledWith(decodedBytes);
109+
expect(mockMD5Instance.digest).toHaveBeenCalledTimes(2);
110+
expect(base64Encoder).toHaveBeenCalledTimes(2);
111+
expect(base64Encoder).toHaveBeenCalledWith(md5Hash);
112+
});
113+
114+
it("should handle 32-byte binary key input for SSECustomerKey and CopySourceSSECustomerKey", async () => {
115+
const binaryKey = crypto.randomBytes(32);
116+
const md5Hash = crypto.createHash("md5").update(binaryKey).digest();
117+
const base64EncodedKey = binaryKey.toString("base64");
118+
const base64EncodedMD5Hash = md5Hash.toString("base64");
119+
120+
const mockMD5Constructor = jest.fn().mockReturnValue({
121+
update: jest.fn().mockReturnThis(),
122+
digest: jest.fn().mockReturnValueOnce(md5Hash).mockReturnValueOnce(md5Hash),
123+
});
124+
125+
const base64Encoder = jest
126+
.fn()
127+
.mockReturnValueOnce(base64EncodedKey)
128+
.mockReturnValueOnce(base64EncodedMD5Hash)
129+
.mockReturnValueOnce(base64EncodedKey)
130+
.mockReturnValueOnce(base64EncodedMD5Hash);
131+
78132
const handler = ssecMiddleware({
79-
base64Encoder: encoder2,
133+
base64Encoder,
80134
utf8Decoder: decoder,
81-
md5: MockHash,
82-
base64Decoder: base64Decoder,
135+
md5: mockMD5Constructor,
136+
base64Decoder,
83137
})(next, {} as any);
84138

139+
const args = {
140+
input: {
141+
SSECustomerKey: binaryKey,
142+
CopySourceSSECustomerKey: binaryKey,
143+
},
144+
};
145+
85146
await handler(args);
86147

87-
expect(next.mock.calls.length).toBe(1);
148+
expect(next).toHaveBeenCalledTimes(1);
88149
expect(next).toHaveBeenCalledWith({
89150
input: {
90-
SSECustomerKey: "base64",
91-
SSECustomerKeyMD5: "base64",
92-
CopySourceSSECustomerKey: "base64",
93-
CopySourceSSECustomerKeyMD5: "base64",
151+
SSECustomerKey: base64EncodedKey,
152+
SSECustomerKeyMD5: base64EncodedMD5Hash,
153+
CopySourceSSECustomerKey: base64EncodedKey,
154+
CopySourceSSECustomerKeyMD5: base64EncodedMD5Hash,
94155
},
95156
});
96-
expect(decoder.mock.calls.length).toBe(2);
97-
expect(encoder2.mock.calls.length).toBe(4);
98-
expect(mockHashUpdate.mock.calls.length).toBe(2);
99-
expect(mockHashDigest.mock.calls.length).toBe(2);
100-
encoder2.mockClear();
157+
158+
expect(mockMD5Constructor).toHaveBeenCalledTimes(2);
159+
expect(base64Encoder).toHaveBeenCalledTimes(4);
160+
});
161+
162+
it("should return false for an invalid base64 string", () => {
163+
const invalidBase64 = "invalid!@#$%";
164+
const base64Decoder = jest.fn();
165+
const options = { base64Decoder };
166+
167+
const result = isValidBase64EncodedSSECustomerKey(invalidBase64, options as any);
168+
expect(result).toBe(false);
169+
expect(base64Decoder).not.toHaveBeenCalled();
170+
});
171+
172+
it("should return true for a valid base64 string and 32 bytes", () => {
173+
const validBase64EncodedSSECustomerKey = "QUIwMTIzNDU2Nzg5QUJDREVGQUJDREVGQUJDREVGQUI=";
174+
const decodedBytes = new Uint8Array(32);
175+
const base64Decoder = jest.fn().mockReturnValue(decodedBytes);
176+
const options = { base64Decoder };
177+
178+
const result = isValidBase64EncodedSSECustomerKey(validBase64EncodedSSECustomerKey, options as any);
179+
expect(result).toBe(true);
180+
expect(base64Decoder).toHaveBeenCalledTimes(1);
181+
expect(base64Decoder).toHaveBeenCalledWith(validBase64EncodedSSECustomerKey);
182+
});
183+
184+
it("should return false for a valid base64 string but not 32 bytes", () => {
185+
const validBase64NonThirtyTwoBytes = "SGVsbG8=";
186+
const base64Decoder = jest.fn().mockReturnValue(new Uint8Array([72, 101, 108, 108, 111]));
187+
const options = { base64Decoder };
188+
189+
const result = isValidBase64EncodedSSECustomerKey(validBase64NonThirtyTwoBytes, options as any);
190+
expect(result).toBe(false);
191+
expect(base64Decoder).toHaveBeenCalledTimes(1);
192+
expect(base64Decoder).toHaveBeenCalledWith(validBase64NonThirtyTwoBytes);
101193
});
102194
});

packages/middleware-ssec/src/index.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ export function ssecMiddleware(options: PreviouslyResolved): InitializeMiddlewar
3939
if (value) {
4040
let valueForHash: Uint8Array;
4141
if (typeof value === "string") {
42-
const isBase64Encoded = /^(?:[A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
43-
if (isBase64Encoded) {
42+
if (isValidBase64EncodedSSECustomerKey(value, options)) {
4443
valueForHash = options.base64Decoder(value);
4544
} else {
4645
valueForHash = options.utf8Decoder(value);
@@ -78,3 +77,16 @@ export const getSsecPlugin = (config: PreviouslyResolved): Pluggable<any, any> =
7877
clientStack.add(ssecMiddleware(config), ssecMiddlewareOptions);
7978
},
8079
});
80+
81+
export function isValidBase64EncodedSSECustomerKey(str: string, options: PreviouslyResolved) {
82+
const base64Regex = /^(?:[A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
83+
84+
if (!base64Regex.test(str)) return false;
85+
86+
try {
87+
const decodedBytes = options.base64Decoder(str);
88+
return decodedBytes.length === 32;
89+
} catch {
90+
return false;
91+
}
92+
}

0 commit comments

Comments
 (0)