Skip to content

Commit 4be2cc6

Browse files
committed
ARSN-552: don't throw in case of bad Date inputs
1 parent af610f2 commit 4be2cc6

5 files changed

Lines changed: 297 additions & 29 deletions

File tree

lib/auth/auth.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@ function generateV4Headers(
216216
payload?: string,
217217
) {
218218
Object.assign(request, { headers: {} });
219-
const amzDate = convertUTCtoISO8601(Date.now());
219+
// Date.now() should always return a valid date so we assert non null.
220+
const amzDate: string = convertUTCtoISO8601(Date.now())!;
221+
220222
// get date without time
221223
const scopeDate = amzDate.slice(0, amzDate.indexOf('T'));
222224
const region = 'us-east-1';

lib/auth/v4/headerAuthCheck.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
checkTimeSkew,
77
convertUTCtoISO8601,
88
convertAmzTimeToMs,
9+
isValidISO8601Compact,
910
} from './timeUtils';
1011
import {
1112
extractAuthItems,
@@ -81,21 +82,18 @@ export function check(
8182
let timestamp: string | undefined;
8283
// check request timestamp
8384
const xAmzDate = request.headers['x-amz-date'];
84-
if (xAmzDate) {
85-
const xAmzDateArr = xAmzDate.split('T');
86-
// check that x-amz- date has the correct format and after epochTime
87-
if (xAmzDateArr.length === 2 && xAmzDateArr[0].length === 8
88-
&& xAmzDateArr[1].length === 7
89-
&& Number.parseInt(xAmzDateArr[0], 10) > 19700101) {
90-
// format of x-amz- date is ISO 8601: YYYYMMDDTHHMMSSZ
91-
timestamp = request.headers['x-amz-date'];
92-
}
85+
if (isValidISO8601Compact(xAmzDate)) {
86+
timestamp = xAmzDate;
9387
} else if (request.headers.date) {
94-
timestamp = convertUTCtoISO8601(request.headers.date);
88+
if (isValidISO8601Compact(request.headers.date)) {
89+
timestamp = request.headers.date;
90+
} else {
91+
timestamp = convertUTCtoISO8601(request.headers.date);
92+
}
9593
}
9694
if (!timestamp) {
9795
log.debug('missing or invalid date header',
98-
{ method: 'auth/v4/headerAuthCheck.check' });
96+
{ 'method': 'auth/v4/headerAuthCheck.check', 'x-amz-date': xAmzDate, 'Date': request.headers.date });
9997
return { err: errorInstances.AccessDenied.
10098
customizeDescription('Authentication requires a valid Date or ' +
10199
'x-amz-date header') };

lib/auth/v4/timeUtils.ts

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,33 @@ export function convertAmzTimeToMs(timestamp: string) {
1515
}
1616

1717
/**
18-
* Convert UTC timestamp to ISO 8601 timestamp
19-
* @param timestamp of UTC form: Fri, 10 Feb 2012 21:34:55 GMT
20-
* @return ISO8601 timestamp of form: YYYYMMDDTHHMMSSZ
21-
*/
22-
export function convertUTCtoISO8601(timestamp: string | number) {
23-
// convert to ISO string: YYYY-MM-DDTHH:mm:ss.sssZ.
24-
const converted = new Date(timestamp).toISOString();
25-
// Remove "-"s and "."s and milliseconds
26-
return converted.split('.')[0].replace(/-|:/g, '').concat('Z');
18+
* Convert UTC timestamp to ISO 8601 compact format
19+
* @param timestamp - UTC timestamp (e.g., 'Fri, 10 Feb 2012 21:34:55 GMT') or Unix timestamp
20+
* @return ISO8601 timestamp of form YYYYMMDDTHHMMSSZ, or undefined if invalid
21+
*
22+
* @example
23+
* convertUTCtoISO8601('Fri, 10 Feb 2012 21:34:55 GMT'); // '20120210T213455Z'
24+
* convertUTCtoISO8601(1328910895000); // '20120210T213455Z'
25+
* convertUTCtoISO8601('invalid'); // undefined
26+
*/
27+
export function convertUTCtoISO8601(timestamp: unknown): string | undefined {
28+
if (timestamp == null) {
29+
return undefined;
30+
}
31+
32+
const date = new Date(timestamp as string | number);
33+
34+
if (isNaN(date.getTime())) {
35+
return undefined;
36+
}
37+
38+
try {
39+
// Can throw RangeError.
40+
const converted = date.toISOString();
41+
return converted.split('.')[0].replace(/-|:/g, '').concat('Z');
42+
} catch {
43+
return undefined;
44+
}
2745
}
2846

2947
/**
@@ -41,16 +59,85 @@ export function checkTimeSkew(timestamp: string, expiry: number, log: RequestLog
4159
if ((currentTime + fifteenMinutes) < parsedTimestamp) {
4260
log.debug('current time pre-dates timestamp', {
4361
parsedTimestamp,
44-
currentTimeInMilliseconds: currentTime });
62+
currentTimeInMilliseconds: currentTime
63+
});
4564
return true;
4665
}
4766
const expiryInMilliseconds = expiry * 1000;
4867
if (currentTime > parsedTimestamp + expiryInMilliseconds) {
4968
log.debug('signature has expired', {
5069
parsedTimestamp,
5170
expiry,
52-
currentTimeInMilliseconds: currentTime });
71+
currentTimeInMilliseconds: currentTime
72+
});
5373
return true;
5474
}
5575
return false;
5676
}
77+
78+
/**
79+
* Validates if a string is in ISO 8601 compact format: YYYYMMDDTHHMMSSZ
80+
*
81+
* Checks that:
82+
* - String is exactly 16 characters long
83+
* - Format matches YYYYMMDDTHHMMSSZ (8 digits, 'T', 6 digits, 'Z')
84+
* - All date/time components are valid (no Feb 30th, no 25:00:00, etc.)
85+
* - No silent date corrections occur (prevents rollover)
86+
*
87+
* @param str - The string to validate
88+
* @returns true if the string is a valid ISO 8601 compact format, false otherwise
89+
*
90+
* @example
91+
* ```typescript
92+
* isValidISO8601Compact('20160208T201405Z'); // true
93+
* isValidISO8601Compact('20160230T201405Z'); // false (Feb 30 invalid)
94+
* isValidISO8601Compact('20160208T251405Z'); // false (25 hours invalid)
95+
* isValidISO8601Compact('2016-02-08T20:14:05Z'); // false (wrong format)
96+
* isValidISO8601Compact('abcd0208T201405Z'); // false (contains letters)
97+
* ```
98+
*/
99+
export function isValidISO8601Compact(str: string): boolean {
100+
if (str == null || typeof str !== 'string') {
101+
return false;
102+
}
103+
104+
// Check length
105+
if (str.length !== 16) {
106+
return false;
107+
}
108+
109+
// Check 'T' at position 8 and 'Z' at position 15
110+
if (str[8] !== 'T' || str[15] !== 'Z') {
111+
return false;
112+
}
113+
114+
// Check all other positions are digits
115+
for (let i = 0; i < 16; i++) {
116+
if (i === 8 || i === 15) {
117+
continue; // Skip T and Z
118+
}
119+
if (str[i] < '0' || str[i] > '9') {
120+
return false;
121+
}
122+
}
123+
124+
// Extract components
125+
const year = str.substring(0, 4);
126+
const month = str.substring(4, 6);
127+
const day = str.substring(6, 8);
128+
const hour = str.substring(9, 11);
129+
const minute = str.substring(11, 13);
130+
const second = str.substring(13, 15);
131+
132+
// Construct standard ISO format and validate
133+
const isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}.000Z`;
134+
const date = new Date(isoString);
135+
136+
try {
137+
// date.toISOString() can throw.
138+
// date.toISOString() === isoString check prevents silent date corrections (30 February to 1 March)
139+
return !isNaN(date.getTime()) && date.toISOString() === isoString;
140+
} catch {
141+
return false;
142+
}
143+
}

tests/unit/auth/v4/headerAuthCheck.spec.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,19 +179,17 @@ describe('v4 headerAuthCheck', () => {
179179

180180
it('should return error if timestamp from x-amz-date header' +
181181
'is before epochTime', done => {
182-
// Different date (2095 instead of 2016)
182+
// Date from 1950 (before epoch time)
183183
const alteredRequest = createAlteredRequest({
184184
'x-amz-date': '19500707T215304Z',
185185
'authorization': 'AWS4-HMAC-SHA256 Credential' +
186-
'=accessKey1/20160208/us-east-1/s3/aws4_request, ' +
186+
'=accessKey1/19500707/us-east-1/s3/aws4_request, ' +
187187
'SignedHeaders=host;x-amz-content-sha256;' +
188188
'x-amz-date, Signature=abed924c06abf8772c67' +
189189
'0064d22eacd6ccb85c06befa15f' +
190190
'4a789b0bae19307bc' }, 'headers', request, headers);
191191
const res = headerAuthCheck(alteredRequest, log);
192-
assert.deepStrictEqual(res.err, errorInstances.AccessDenied.
193-
customizeDescription('Authentication requires a valid Date or ' +
194-
'x-amz-date header'));
192+
assert.deepStrictEqual(res.err, errors.RequestTimeTooSkewed);
195193
done();
196194
});
197195

0 commit comments

Comments
 (0)