From 6a568af0efab0d78198c72ac4d0039e89572edd4 Mon Sep 17 00:00:00 2001 From: Will Toozs Date: Mon, 24 Jun 2024 13:13:12 +0200 Subject: [PATCH 1/7] ARSN-422: add objectPost callApiMethod --- lib/s3routes/routes/routePOST.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/s3routes/routes/routePOST.ts b/lib/s3routes/routes/routePOST.ts index 1854d4b97..d3581324a 100644 --- a/lib/s3routes/routes/routePOST.ts +++ b/lib/s3routes/routes/routePOST.ts @@ -58,6 +58,10 @@ export default function routePOST( corsHeaders)); } + if (objectKey === undefined && Object.keys(query).length === 0) { + return api.callApiMethod('objectPost', request, response, log, (err, resHeaders) => routesUtils.responseNoBody(err, resHeaders, response, 204, log)); + } + return routesUtils.responseNoBody(errors.NotImplemented, null, response, 200, log); } From 1016c270856ffae3071cba0237cee079888d3085 Mon Sep 17 00:00:00 2001 From: Will Toozs Date: Fri, 28 Jun 2024 12:42:33 +0200 Subject: [PATCH 2/7] ARSN-422: update max post field length error --- lib/errors/arsenalErrors.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/errors/arsenalErrors.ts b/lib/errors/arsenalErrors.ts index 391191bf2..52bcebf30 100644 --- a/lib/errors/arsenalErrors.ts +++ b/lib/errors/arsenalErrors.ts @@ -281,10 +281,10 @@ export const MaxMessageLengthExceeded: ErrorFormat = { description: 'Your request was too big.', }; -export const MaxPostPreDataLengthExceededError: ErrorFormat = { +export const MaxPostPreDataLengthExceeded: ErrorFormat = { code: 400, description: - 'Your POST request fields preceding the upload file were too large.', + 'Your POST request fields preceeding the upload file was too large.', }; export const MetadataTooLarge: ErrorFormat = { From 2123a0bb8370885d3fc2f691e5ad5cbf9f5ccaa4 Mon Sep 17 00:00:00 2001 From: Will Toozs Date: Thu, 4 Jul 2024 15:19:12 +0200 Subject: [PATCH 3/7] ARSN-424: prep doAuth for postObject --- lib/auth/auth.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/auth/auth.ts b/lib/auth/auth.ts index 3344cd69e..659ebe8e6 100644 --- a/lib/auth/auth.ts +++ b/lib/auth/auth.ts @@ -25,6 +25,7 @@ const checkFunctions = { v4: { headers: v4.header.check, query: v4.query.check, + form: v4.form.check, }, }; @@ -63,7 +64,7 @@ function extractParams( log.trace('entered', { method: 'Arsenal.auth.server.extractParams' }); const authHeader = request.headers.authorization; let version: 'v2' |'v4' | null = null; - let method: 'query' | 'headers' | null = null; + let method: 'query' | 'headers' | 'form' | null = null; // Identify auth version and method to dispatch to the right check function if (authHeader) { @@ -85,6 +86,9 @@ function extractParams( } else if (data['X-Amz-Algorithm']) { method = 'query'; version = 'v4'; + } if (data.policy) { + method = 'form'; + version = 'v4'; } // Here, either both values are set, or none is set @@ -121,7 +125,8 @@ function doAuth( awsService: string, requestContexts: any[] | null ) { - const res = extractParams(request, log, awsService, request.query); + const data: { [key: string]: string; } = request.formData || request.query || {}; + const res = extractParams(request, log, awsService, data); if (res.err) { return cb(res.err); } else if (res.params instanceof AuthInfo) { From c2ac325a270d16b5845e2f0b149155042a252d13 Mon Sep 17 00:00:00 2001 From: Will Toozs Date: Thu, 4 Jul 2024 15:19:58 +0200 Subject: [PATCH 4/7] ARSN-424: postObject form v4 checks --- lib/auth/v4/authV4.ts | 1 + lib/auth/v4/formAuthCheck.ts | 72 +++++++++++++++++++++++++++++++++++ lib/auth/v4/validateInputs.ts | 69 +++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 lib/auth/v4/formAuthCheck.ts diff --git a/lib/auth/v4/authV4.ts b/lib/auth/v4/authV4.ts index b968aca7d..d3c568b36 100644 --- a/lib/auth/v4/authV4.ts +++ b/lib/auth/v4/authV4.ts @@ -1,2 +1,3 @@ export * as header from './headerAuthCheck'; export * as query from './queryAuthCheck'; +export * as form from './formAuthCheck'; diff --git a/lib/auth/v4/formAuthCheck.ts b/lib/auth/v4/formAuthCheck.ts new file mode 100644 index 000000000..33d0fc072 --- /dev/null +++ b/lib/auth/v4/formAuthCheck.ts @@ -0,0 +1,72 @@ +import { Logger } from 'werelogs'; +import * as constants from '../../constants'; +import errors from '../../errors'; +import { convertAmzTimeToMs } from './timeUtils'; +import { validateCredentials, extractFormParams } from './validateInputs'; + +/** + * V4 query auth check + * @param request - HTTP request object + * @param log - logging object + * @param data - Contain authentification params (GET or POST data) + */ +export function check(request: any, log: Logger, data: { [key: string]: string }) { + const authParams = extractFormParams(data, log); + + if (Object.keys(authParams).length !== 4) { + return { err: errors.InvalidArgument }; + } + + const token = data['x-amz-security-token']; + if (token && !constants.iamSecurityToken.pattern.test(token)) { + log.debug('invalid security token', { token }); + return { err: errors.InvalidToken }; + } + + // const signedHeaders = authParams.signedHeaders!; + const signatureFromRequest = authParams.signatureFromRequest!; + const timestamp = authParams.timestamp!; + const expiration = authParams.expiration!; + const credential = authParams.credential!; + + // check if the expiration date is passed the current time + if (Date.parse(expiration) < Date.now()) { + return { err: errors.RequestExpired }; + } + + const validationResult = validateCredentials(credential, timestamp, + log); + if (validationResult instanceof Error) { + log.debug('credentials in improper format', { credential, + timestamp, validationResult }); + return { err: validationResult }; + } + const accessKey = credential[0]; + const scopeDate = credential[1]; + const region = credential[2]; + const service = credential[3]; + + // string to sign is the policy for form requests + const stringToSign = data['policy']; + + log.trace('constructed stringToSign', { stringToSign }); + return { + err: null, + params: { + version: 4, + data: { + accessKey, + signatureFromRequest, + region, + scopeDate, + stringToSign, + service, + authType: 'REST-FORM-DATA', + signatureVersion: 'AWS4-HMAC-SHA256', + signatureAge: Date.now() - convertAmzTimeToMs(timestamp), + timestamp, + securityToken: token, + }, + }, + }; +} diff --git a/lib/auth/v4/validateInputs.ts b/lib/auth/v4/validateInputs.ts index 4d83fba24..346d05dfd 100644 --- a/lib/auth/v4/validateInputs.ts +++ b/lib/auth/v4/validateInputs.ts @@ -1,5 +1,7 @@ import { Logger } from 'werelogs'; import errors from '../../../lib/errors'; +import { auth } from '../../..'; +import { String } from 'aws-sdk/clients/cloudwatchevents'; /** * Validate Credentials @@ -130,6 +132,73 @@ export function extractQueryParams( return authParams; } +/** + * Extract and validate components from formData object + * @param formObj - formData object from request + * @param log - logging object + * @return object containing extracted query params for authV4 + */ +export function extractFormParams( + formObj: { [key: string]: string | undefined }, + log: Logger +) { + const authParams: { + signedHeaders?: string; + signatureFromRequest?: string; + timestamp?: string; + expiration?: String; + credential?: [string, string, string, string, string]; + } = {}; + + // Do not need the algorithm sent back + if (formObj['x-amz-algorithm'] !== 'AWS4-HMAC-SHA256') { + log.warn('algorithm param incorrect', { algo: formObj['X-Amz-Algorithm'] }); + return authParams; + } + + // // adding placeholder for signedHeaders to satisfy Vault + // // as this is not required for form auth + // authParams.signedHeaders = 'content-type;host;x-amz-date;x-amz-security-token'; + + const signature = formObj['x-amz-signature']; + if (signature && signature.length === 64) { + authParams.signatureFromRequest = signature; + } else { + log.warn('missing signature'); + return authParams; + } + + const timestamp = formObj['x-amz-date']; + if (timestamp && timestamp.length === 16) { + authParams.timestamp = timestamp; + } else { + log.warn('missing or invalid timestamp', { timestamp: formObj['x-amz-date'] }); + return authParams; + } + + const policy = formObj['policy']; + if (policy && policy.length > 0) { + const decryptedPolicy = Buffer.from(policy, 'base64').toString('utf8'); + const policyObj = JSON.parse(decryptedPolicy); + const expiration = policyObj.expiration; + authParams.expiration = expiration; + } else { + log.warn('missing or invalid policy', { policy: formObj['policy'] }); + return authParams; + } + + const credential = formObj['x-amz-credential']; + if (credential && credential.length > 28 && credential.indexOf('/') > -1) { + // @ts-ignore + authParams.credential = credential.split('/'); + } else { + log.warn('invalid credential param', { credential: formObj['X-Amz-Credential'] }); + return authParams; + } + + return authParams; +} + /** * Extract and validate components from auth header From 6ff190a642f8edffcae1d47b4b8a1ae64fc760a3 Mon Sep 17 00:00:00 2001 From: Will Toozs Date: Thu, 4 Jul 2024 18:17:35 +0200 Subject: [PATCH 5/7] test setup form Data --- lib/auth/v4/formAuthCheck.ts | 47 ++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/lib/auth/v4/formAuthCheck.ts b/lib/auth/v4/formAuthCheck.ts index 33d0fc072..3fbd478b7 100644 --- a/lib/auth/v4/formAuthCheck.ts +++ b/lib/auth/v4/formAuthCheck.ts @@ -11,9 +11,46 @@ import { validateCredentials, extractFormParams } from './validateInputs'; * @param data - Contain authentification params (GET or POST data) */ export function check(request: any, log: Logger, data: { [key: string]: string }) { - const authParams = extractFormParams(data, log); + let signatureFromRequest; + let timestamp; + let expiration; + let credential; - if (Object.keys(authParams).length !== 4) { + if (data['x-amz-algorithm'] !== 'AWS4-HMAC-SHA256') { + log.debug('algorithm param incorrect', { algo: data['X-Amz-Algorithm'] }); + } + + // // adding placeholder for signedHeaders to satisfy Vault + // // as this is not required for form auth + // authParams.signedHeaders = 'content-type;host;x-amz-date;x-amz-security-token'; + + signatureFromRequest = data['x-amz-signature']; + if (!signatureFromRequest || signatureFromRequest.length !== 64) { + log.debug('missing signature'); + } + + timestamp = data['x-amz-date']; + if (!timestamp || timestamp.length !== 16) { + log.debug('missing or invalid timestamp', { timestamp: data['x-amz-date'] }); + return { err: errors.InvalidArgument }; + } + + const policy = data['policy']; + if (policy && policy.length > 0) { + const decryptedPolicy = Buffer.from(policy, 'base64').toString('utf8'); + const policyObj = JSON.parse(decryptedPolicy); + expiration = policyObj.expiration; + } else { + log.debug('missing or invalid policy', { policy: data['policy'] }); + return { err: errors.InvalidArgument }; + } + + credential = data['x-amz-credential']; + if (credential && credential.length > 28 && credential.indexOf('/') > -1) { + // @ts-ignore + credential = credential.split('/'); + } else { + log.debug('invalid credential param', { credential: data['X-Amz-Credential'] }); return { err: errors.InvalidArgument }; } @@ -23,12 +60,6 @@ export function check(request: any, log: Logger, data: { [key: string]: string } return { err: errors.InvalidToken }; } - // const signedHeaders = authParams.signedHeaders!; - const signatureFromRequest = authParams.signatureFromRequest!; - const timestamp = authParams.timestamp!; - const expiration = authParams.expiration!; - const credential = authParams.credential!; - // check if the expiration date is passed the current time if (Date.parse(expiration) < Date.now()) { return { err: errors.RequestExpired }; From b00aea282244cd90efc745941e03d0be7e734fc7 Mon Sep 17 00:00:00 2001 From: Will Toozs Date: Thu, 4 Jul 2024 19:01:19 +0200 Subject: [PATCH 6/7] test setup form Data --- lib/auth/v4/formAuthCheck.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/auth/v4/formAuthCheck.ts b/lib/auth/v4/formAuthCheck.ts index 3fbd478b7..c9e71062e 100644 --- a/lib/auth/v4/formAuthCheck.ts +++ b/lib/auth/v4/formAuthCheck.ts @@ -20,13 +20,10 @@ export function check(request: any, log: Logger, data: { [key: string]: string } log.debug('algorithm param incorrect', { algo: data['X-Amz-Algorithm'] }); } - // // adding placeholder for signedHeaders to satisfy Vault - // // as this is not required for form auth - // authParams.signedHeaders = 'content-type;host;x-amz-date;x-amz-security-token'; - signatureFromRequest = data['x-amz-signature']; - if (!signatureFromRequest || signatureFromRequest.length !== 64) { + if (!signatureFromRequest) { log.debug('missing signature'); + return { err: errors.InvalidArgument }; } timestamp = data['x-amz-date']; @@ -49,6 +46,13 @@ export function check(request: any, log: Logger, data: { [key: string]: string } if (credential && credential.length > 28 && credential.indexOf('/') > -1) { // @ts-ignore credential = credential.split('/'); + const validationResult = validateCredentials(credential, timestamp, + log); + if (validationResult instanceof Error) { + log.debug('credentials in improper format', { credential, + timestamp, validationResult }); + return { err: validationResult }; + } } else { log.debug('invalid credential param', { credential: data['X-Amz-Credential'] }); return { err: errors.InvalidArgument }; From 02c9b14df51b1ab626fccdf43019a26b9f37ae11 Mon Sep 17 00:00:00 2001 From: Will Toozs Date: Mon, 8 Jul 2024 19:16:35 +0200 Subject: [PATCH 7/7] unit tests --- lib/auth/v4/formAuthCheck.ts | 5 +- tests/unit/auth/v4/formAuthCheck.spec.js | 172 +++++++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 tests/unit/auth/v4/formAuthCheck.spec.js diff --git a/lib/auth/v4/formAuthCheck.ts b/lib/auth/v4/formAuthCheck.ts index c9e71062e..deb1015f5 100644 --- a/lib/auth/v4/formAuthCheck.ts +++ b/lib/auth/v4/formAuthCheck.ts @@ -18,6 +18,7 @@ export function check(request: any, log: Logger, data: { [key: string]: string } if (data['x-amz-algorithm'] !== 'AWS4-HMAC-SHA256') { log.debug('algorithm param incorrect', { algo: data['X-Amz-Algorithm'] }); + return { err: errors.InvalidArgument }; } signatureFromRequest = data['x-amz-signature']; @@ -64,9 +65,9 @@ export function check(request: any, log: Logger, data: { [key: string]: string } return { err: errors.InvalidToken }; } - // check if the expiration date is passed the current time + // check if the expiration date is past the current time if (Date.parse(expiration) < Date.now()) { - return { err: errors.RequestExpired }; + return { err: errors.AccessDenied.customizeDescription('Invalid according to Policy: Policy expired.') }; } const validationResult = validateCredentials(credential, timestamp, diff --git a/tests/unit/auth/v4/formAuthCheck.spec.js b/tests/unit/auth/v4/formAuthCheck.spec.js new file mode 100644 index 000000000..d924e51fc --- /dev/null +++ b/tests/unit/auth/v4/formAuthCheck.spec.js @@ -0,0 +1,172 @@ +'use strict'; // eslint-disable-line strict + +const assert = require('assert'); +const fakeTimers = require('@sinonjs/fake-timers'); + +const errors = require('../../../../lib/errors').default; + +const createAlteredRequest = require('../../helpers').createAlteredRequest; +const formAuthCheck = require('../../../../lib/auth/v4/formAuthCheck').check; +const DummyRequestLogger = require('../../helpers').DummyRequestLogger; + +const log = new DummyRequestLogger(); + +const method = 'POST'; +const path = decodeURIComponent('/mybucket'); +const host = 'localhost:8000'; + +const formatDate = now => now.toISOString().replace(/[:-]|\.\d{3}/g, ''); + +const requestDate = new Date(Date.now()); + +function prepPolicy(data, expiration = new Date(requestDate.getTime() + 15 * 60 * 1000)) { + try { + // 15 minutes + const policy = { expiration: expiration.toISOString() }; + policy.conditions = Object.keys(data).map(key => ({ key: data[key] })); + // return base64 version of policy + return policy; + } catch (e) { + console.error('Policy is not a valid JSON', e); + throw new Error('Policy is not a valid JSON'); + } +} + +const formData = { + 'x-amz-algorithm': 'AWS4-HMAC-SHA256', + 'x-amz-credential': `accessKey1/${formatDate(requestDate).split('T')[0]}/us-east-1/s3/aws4_request`, + 'x-amz-date': formatDate(requestDate), + 'x-amz-signature': '036c5d854aca98a003c1c155a' + + '7723157d8148ad5888b3aee1133784eb5aec08b', +}; +formData.policy = `${btoa(JSON.stringify(prepPolicy(formData)))}`; + +const headers = { + host, +}; +const request = { + method, + path, + headers, + formData, +}; + +describe('v4 formAuthCheck', () => { + it('should return error if algorithm param incorrect', done => { + const alteredRequest = createAlteredRequest({ + 'x-amz-algorithm': + 'AWS4-HMAC-SHA1', + }, 'formData', request, formData); + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + assert.deepStrictEqual(res.err, errors.InvalidArgument); + done(); + }); + + it('should return error if x-amz-credential param is undefined', done => { + const alteredRequest = createAlteredRequest({ + 'x-amz-credential': + undefined, + }, 'formData', request, formData); + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + assert.deepStrictEqual(res.err, errors.InvalidArgument); + done(); + }); + + it('should return error if credential param format incorrect', done => { + const alteredRequest = createAlteredRequest({ + 'x-amz-credential': + 'incorrectformat', + }, 'formData', request, formData); + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + assert.deepStrictEqual(res.err, errors.InvalidArgument); + done(); + }); + + it('should return error if service set forth in ' + + 'credential param is not s3', done => { + const alteredRequest = createAlteredRequest({ + 'x-amz-credential': + `accessKey1/${formatDate(requestDate).split('T')[0]}/us-east-1/EC2/aws4_request` + }, + 'formData', request, formData); + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + assert.deepStrictEqual(res.err, errors.InvalidArgument); + done(); + }); + + it('should return error if requestType set forth in ' + + 'credential param is not aws4_request', done => { + const alteredRequest = createAlteredRequest({ + 'x-amz-credential': + `accessKey1/${formatDate(requestDate).split('T')[0]}/us-east-1/s3/aws2_request` + }, + 'formData', request, formData); + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + assert.deepStrictEqual(res.err, errors.InvalidArgument); + done(); + }); + + it('should return error if undefined x-amz-signature param', done => { + const alteredRequest = createAlteredRequest({ + 'x-amz-signature': + undefined + }, 'formData', request, formData); + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + assert.deepStrictEqual(res.err, errors.InvalidArgument); + done(); + }); + + it('should return error if undefined x-amz-date param', done => { + const alteredRequest = createAlteredRequest({ + 'x-amz-date': + undefined + }, 'formData', request, formData); + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + assert.deepStrictEqual(res.err, errors.InvalidArgument); + done(); + }); + + it('should return error if expiration param is too old', done => { + const expiredDate = new Date(Date.now() - 30 * 60 * 1000); + + // Update the expiration date in formData + const alteredFormData = { + ...formData, + policy: `${btoa(JSON.stringify(prepPolicy(formData, expiredDate)))}` + }; + + // Assuming alteredRequest is the request object that includes formData + const alteredRequest = { + ...request, + formData: alteredFormData + }; + + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + assert.deepStrictEqual(res.err, errors.AccessDenied); + done(); + }); + + it('should return error if scope date from x-amz-credential param' + + 'does not match date from x-amz-date param', done => { + const clock = fakeTimers.install({ now: 1454974984001 }); + const alteredRequest = createAlteredRequest({ + 'x-amz-credential': 'accessKey1/20160209/' + + 'us-east-1/s3/aws4_request', + }, 'formData', request, formData); + const res = formAuthCheck(alteredRequest, log, alteredRequest.formData); + clock.uninstall(); + assert.deepStrictEqual(res.err, errors.RequestTimeTooSkewed); + done(); + }); + + it('should successfully return v4 and no error', done => { + // Freezes time so date created within function will be Feb 8, 2016 + // (within 15 minutes of timestamp in request) + const clock = fakeTimers.install({ now: 1454974984001 }); + const res = formAuthCheck(request, log, request.formData); + clock.uninstall(); + assert.deepStrictEqual(res.err, null); + assert.strictEqual(res.params.version, 4); + done(); + }); +});