Compare commits

...

7 Commits

Author SHA1 Message Date
Will Toozs 02c9b14df5
unit tests 2024-07-08 19:16:35 +02:00
Will Toozs b00aea2822
test setup form Data 2024-07-04 19:01:19 +02:00
Will Toozs 6ff190a642
test setup form Data 2024-07-04 18:17:35 +02:00
Will Toozs c2ac325a27
ARSN-424: postObject form v4 checks 2024-07-04 16:33:51 +02:00
Will Toozs 2123a0bb83
ARSN-424: prep doAuth for postObject 2024-07-04 15:19:12 +02:00
Will Toozs 1016c27085
ARSN-422: update max post field length error 2024-07-01 17:34:13 +02:00
Will Toozs 6a568af0ef
ARSN-422: add objectPost callApiMethod 2024-07-01 17:34:11 +02:00
7 changed files with 363 additions and 4 deletions

View File

@ -25,6 +25,7 @@ const checkFunctions = {
v4: { v4: {
headers: v4.header.check, headers: v4.header.check,
query: v4.query.check, query: v4.query.check,
form: v4.form.check,
}, },
}; };
@ -63,7 +64,7 @@ function extractParams(
log.trace('entered', { method: 'Arsenal.auth.server.extractParams' }); log.trace('entered', { method: 'Arsenal.auth.server.extractParams' });
const authHeader = request.headers.authorization; const authHeader = request.headers.authorization;
let version: 'v2' |'v4' | null = null; 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 // Identify auth version and method to dispatch to the right check function
if (authHeader) { if (authHeader) {
@ -85,6 +86,9 @@ function extractParams(
} else if (data['X-Amz-Algorithm']) { } else if (data['X-Amz-Algorithm']) {
method = 'query'; method = 'query';
version = 'v4'; version = 'v4';
} if (data.policy) {
method = 'form';
version = 'v4';
} }
// Here, either both values are set, or none is set // Here, either both values are set, or none is set
@ -121,7 +125,8 @@ function doAuth(
awsService: string, awsService: string,
requestContexts: any[] | null 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) { if (res.err) {
return cb(res.err); return cb(res.err);
} else if (res.params instanceof AuthInfo) { } else if (res.params instanceof AuthInfo) {

View File

@ -1,2 +1,3 @@
export * as header from './headerAuthCheck'; export * as header from './headerAuthCheck';
export * as query from './queryAuthCheck'; export * as query from './queryAuthCheck';
export * as form from './formAuthCheck';

View File

@ -0,0 +1,108 @@
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 }) {
let signatureFromRequest;
let timestamp;
let expiration;
let credential;
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'];
if (!signatureFromRequest) {
log.debug('missing signature');
return { err: errors.InvalidArgument };
}
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('/');
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 };
}
const token = data['x-amz-security-token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
return { err: errors.InvalidToken };
}
// check if the expiration date is past the current time
if (Date.parse(expiration) < Date.now()) {
return { err: errors.AccessDenied.customizeDescription('Invalid according to Policy: Policy expired.') };
}
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,
},
},
};
}

View File

@ -1,5 +1,7 @@
import { Logger } from 'werelogs'; import { Logger } from 'werelogs';
import errors from '../../../lib/errors'; import errors from '../../../lib/errors';
import { auth } from '../../..';
import { String } from 'aws-sdk/clients/cloudwatchevents';
/** /**
* Validate Credentials * Validate Credentials
@ -130,6 +132,73 @@ export function extractQueryParams(
return authParams; 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 * Extract and validate components from auth header

View File

@ -281,10 +281,10 @@ export const MaxMessageLengthExceeded: ErrorFormat = {
description: 'Your request was too big.', description: 'Your request was too big.',
}; };
export const MaxPostPreDataLengthExceededError: ErrorFormat = { export const MaxPostPreDataLengthExceeded: ErrorFormat = {
code: 400, code: 400,
description: 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 = { export const MetadataTooLarge: ErrorFormat = {

View File

@ -58,6 +58,10 @@ export default function routePOST(
corsHeaders)); 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, return routesUtils.responseNoBody(errors.NotImplemented, null, response,
200, log); 200, log);
} }

View File

@ -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();
});
});