Skip to content

Commit a9afd5f

Browse files
committed
CLDSRV-620: Ignore trailing checksums in upload requests
1 parent 965a80f commit a9afd5f

File tree

9 files changed

+389
-77
lines changed

9 files changed

+389
-77
lines changed

constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,13 @@ const constants = {
187187
// Session name of the backbeat lifecycle assumed role session.
188188
backbeatLifecycleSessionName: 'backbeat-lifecycle',
189189
unsupportedSignatureChecksums: new Set([
190-
'STREAMING-UNSIGNED-PAYLOAD-TRAILER',
191190
'STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER',
192191
'STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD',
193192
'STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER',
194193
]),
195194
supportedSignatureChecksums: new Set([
196195
'UNSIGNED-PAYLOAD',
196+
'STREAMING-UNSIGNED-PAYLOAD-TRAILER',
197197
'STREAMING-AWS4-HMAC-SHA256-PAYLOAD',
198198
]),
199199
ipv4Regex: /^(\d{1,3}\.){3}\d{1,3}(\/(3[0-2]|[12]?\d))?$/,

lib/api/apiUtils/object/prepareStream.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const V4Transform = require('../../../auth/streamingV4/V4Transform');
2+
const TrailingChecksumTransform = require('../../../auth/streamingV4/trailingChecksumTransform');
23

34
/**
45
* Prepares the stream if the chunks are sent in a v4 Auth request
@@ -24,11 +25,26 @@ function prepareStream(stream, streamingV4Params, log, errCb) {
2425
}
2526
const v4Transform = new V4Transform(streamingV4Params, log, errCb);
2627
stream.pipe(v4Transform);
28+
v4Transform.headers = stream.headers;
2729
return v4Transform;
2830
}
2931
return stream;
3032
}
3133

34+
function stripTrailingChecksumStream(stream, log, errCb) {
35+
// don't do anything if x-amz-trailer has not been set
36+
if (stream.headers['x-amz-trailer'] === undefined ||
37+
stream.headers['x-amz-trailer'] === '') {
38+
return stream;
39+
}
40+
41+
const trailingChecksumTransform = new TrailingChecksumTransform(log, errCb);
42+
stream.pipe(trailingChecksumTransform);
43+
trailingChecksumTransform.headers = stream.headers;
44+
return trailingChecksumTransform;
45+
}
46+
3247
module.exports = {
3348
prepareStream,
49+
stripTrailingChecksumStream,
3450
};

lib/api/apiUtils/object/storeObject.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { errors, jsutil } = require('arsenal');
22

33
const { data } = require('../../../data/wrapper');
4-
const { prepareStream } = require('./prepareStream');
4+
const { prepareStream, stripTrailingChecksumStream } = require('./prepareStream');
55

66
/**
77
* Check that `hashedStream.completedHash` matches header `stream.contentMD5`
@@ -58,10 +58,11 @@ function checkHashMatchMD5(stream, hashedStream, dataRetrievalInfo, log, cb) {
5858
function dataStore(objectContext, cipherBundle, stream, size,
5959
streamingV4Params, backendInfo, log, cb) {
6060
const cbOnce = jsutil.once(cb);
61-
const dataStream = prepareStream(stream, streamingV4Params, log, cbOnce);
62-
if (!dataStream) {
61+
const dataStreamTmp = prepareStream(stream, streamingV4Params, log, cbOnce);
62+
if (!dataStreamTmp) {
6363
return process.nextTick(() => cb(errors.InvalidArgument));
6464
}
65+
const dataStream = stripTrailingChecksumStream(dataStreamTmp, log, cbOnce);
6566
return data.put(
6667
cipherBundle, dataStream, size, objectContext, backendInfo, log,
6768
(err, dataRetrievalInfo, hashedStream) => {

lib/api/apiUtils/object/validateChecksumHeaders.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ const { unsupportedSignatureChecksums, supportedSignatureChecksums } = require('
55
function validateChecksumHeaders(headers) {
66
// If the x-amz-trailer header is present the request is using one of the
77
// trailing checksum algorithms, which are not supported.
8-
if (headers['x-amz-trailer'] !== undefined) {
9-
return errors.BadRequest.customizeDescription('trailing checksum is not supported');
8+
9+
if (headers['x-amz-trailer'] !== undefined &&
10+
headers['x-amz-content-sha256'] !== 'STREAMING-UNSIGNED-PAYLOAD-TRAILER') {
11+
return errors.BadRequest.customizeDescription('signed trailing checksum is not supported');
1012
}
1113

1214
const signatureChecksum = headers['x-amz-content-sha256'];

lib/auth/streamingV4/V4Transform.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ class V4Transform extends Transform {
184184
* @param {Buffer} chunk - chunk from request body
185185
* @param {string} encoding - Data encoding
186186
* @param {function} callback - Callback(err, justDataChunk, encoding)
187-
* @return {function }executes callback with err if applicable
187+
* @return {function} executes callback with err if applicable
188188
*/
189189
_transform(chunk, encoding, callback) {
190190
// 'chunk' here is the node streaming chunk
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const { Transform } = require('stream');
2+
const { errors } = require('arsenal');
3+
4+
/**
5+
* This class is designed to handle the chunks sent in a streaming
6+
* v4 Auth request
7+
*/
8+
class TrailingChecksumTransform extends Transform {
9+
/**
10+
* @constructor
11+
* @param {object} log - logger object
12+
* @param {function} errCb - callback called if an error occurs
13+
*/
14+
constructor(log, errCb) {
15+
super({});
16+
this.log = log;
17+
this.errCb = errCb;
18+
this.chunkSizeBuffer = Buffer.alloc(0);
19+
this.bytesToDiscard = 0; // when trailing \r\n are present, we discard them but they can be in different chunks
20+
this.bytesToRead = 0; // when a chunk is advertised, the size is put here and we forward all bytes
21+
this.streamClosed = false;
22+
}
23+
24+
/**
25+
* This function will remove the trailing checksum from the stream
26+
*
27+
* @param {Buffer} chunkInput - chunk from request body
28+
* @param {string} encoding - Data encoding
29+
* @param {function} callback - Callback(err, justDataChunk, encoding)
30+
* @return {function} executes callback with err if applicable
31+
*/
32+
_transform(chunkInput, encoding, callback) {
33+
let chunk = chunkInput;
34+
while (chunk.byteLength > 0 && !this.streamClosed) {
35+
if (this.bytesToDiscard > 0) {
36+
const toDiscard = Math.min(this.bytesToDiscard, chunk.byteLength);
37+
chunk = chunk.subarray(toDiscard);
38+
this.bytesToDiscard -= toDiscard;
39+
continue;
40+
}
41+
// forward up to bytesToRead bytes from the chunk, restart processing on leftover
42+
if (this.bytesToRead > 0) {
43+
const toRead = Math.min(this.bytesToRead, chunk.byteLength);
44+
this.push(chunk.subarray(0, toRead));
45+
chunk = chunk.subarray(toRead);
46+
this.bytesToRead -= toRead;
47+
if (this.bytesToRead === 0) {
48+
this.bytesToDiscard = 2;
49+
}
50+
continue;
51+
}
52+
53+
const lineBreakIndex = chunk.indexOf('\r');
54+
if (lineBreakIndex === -1) {
55+
if (this.chunkSizeBuffer.byteLength + chunk.byteLength > 10) {
56+
this.log.info('chunk size field too big', {
57+
chunkSizeBuffer: this.chunkSizeBuffer.toString(),
58+
truncatedChunk: chunk.subarray(0, 16).toString(),
59+
});
60+
// if bigger, the chunk would be over 5 GB
61+
// returning early to avoid a DoS by memory exhaustion
62+
return callback(errors.InvalidArgument);
63+
}
64+
// no delimiter, we'll keep the chunk for later
65+
this.chunkSizeBuffer = Buffer.concat([this.chunkSizeBuffer, chunk]);
66+
return callback();
67+
}
68+
69+
this.chunkSizeBuffer = Buffer.concat([this.chunkSizeBuffer, chunk.subarray(0, lineBreakIndex)]);
70+
chunk = chunk.subarray(lineBreakIndex);
71+
72+
// chunk-size is sent in hex
73+
if (!/^[0-9a-fA-F]+$/.test(this.chunkSizeBuffer.toString())) {
74+
this.log.info('chunk size is not a valid hex number', {
75+
chunkSizeBuffer: this.chunkSizeBuffer.toString(),
76+
});
77+
return callback(errors.InvalidArgument);
78+
}
79+
const dataSize = Number.parseInt(this.chunkSizeBuffer.toString(), 16);
80+
if (Number.isNaN(dataSize)) {
81+
this.log.error('unable to parse chunk size', {
82+
chunkSizeBuffer: this.chunkSizeBuffer.toString(),
83+
});
84+
return callback(errors.InvalidArgument);
85+
}
86+
this.chunkSizeBuffer = Buffer.alloc(0);
87+
if (dataSize === 0) {
88+
// TODO: check if the checksum is correct
89+
// last chunk, no more data to read, the stream is closed
90+
this.streamClosed = true;
91+
}
92+
this.bytesToRead = dataSize;
93+
this.bytesToDiscard = 2;
94+
}
95+
96+
return callback();
97+
}
98+
}
99+
100+
module.exports = TrailingChecksumTransform;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
const assert = require('assert');
2+
const async = require('async');
3+
const { makeS3Request } = require('../utils/makeRequest');
4+
const HttpRequestAuthV4 = require('../utils/HttpRequestAuthV4');
5+
6+
const bucket = 'testunsupportedchecksumsbucket';
7+
const objectKey = 'key';
8+
const objData = Buffer.alloc(1024, 'a');
9+
// note this is not the correct checksum in objDataWithTrailingChecksum
10+
const objDataWithTrailingChecksum = '10\r\n0123456789abcdef\r\n' +
11+
'10\r\n0123456789abcdef\r\n' +
12+
'0\r\nx-amz-checksum-crc64nvme:YeIDuLa7tU0=\r\n';
13+
const objDataWithoutTrailingChecksum = '0123456789abcdef0123456789abcdef';
14+
15+
const authCredentials = {
16+
accessKey: 'accessKey1',
17+
secretKey: 'verySecretKey1',
18+
};
19+
20+
const itSkipIfAWS = process.env.AWS_ON_AIR ? it.skip : it;
21+
22+
describe('trailing checksum requests:', () => {
23+
before(done => {
24+
makeS3Request({
25+
method: 'PUT',
26+
authCredentials,
27+
bucket,
28+
}, err => {
29+
assert.ifError(err);
30+
done();
31+
});
32+
});
33+
34+
after(done => {
35+
async.series([
36+
next => makeS3Request({
37+
method: 'DELETE',
38+
authCredentials,
39+
bucket,
40+
objectKey,
41+
}, next),
42+
next => makeS3Request({
43+
method: 'DELETE',
44+
authCredentials,
45+
bucket,
46+
}, next),
47+
], err => {
48+
assert.ifError(err);
49+
done();
50+
});
51+
});
52+
53+
it('should accept unsigned trailing checksum', done => {
54+
const req = new HttpRequestAuthV4(
55+
`http://localhost:8000/${bucket}/${objectKey}`,
56+
Object.assign(
57+
{
58+
method: 'PUT',
59+
headers: {
60+
'content-length': objDataWithTrailingChecksum.length,
61+
'x-amz-decoded-content-length': objDataWithoutTrailingChecksum.length,
62+
'x-amz-content-sha256': 'STREAMING-UNSIGNED-PAYLOAD-TRAILER',
63+
'x-amz-trailer': 'x-amz-checksum-crc64nvme',
64+
},
65+
},
66+
authCredentials
67+
),
68+
res => {
69+
assert.strictEqual(res.statusCode, 200);
70+
res.on('data', () => {});
71+
res.on('end', done);
72+
}
73+
);
74+
75+
req.on('error', err => {
76+
assert.ifError(err);
77+
});
78+
79+
req.write(objDataWithTrailingChecksum);
80+
81+
req.once('drain', () => {
82+
req.end();
83+
});
84+
});
85+
86+
it('should have correct object content for unsigned trailing checksum', done => {
87+
makeS3Request({
88+
method: 'GET',
89+
authCredentials,
90+
bucket,
91+
objectKey,
92+
}, (err, res) => {
93+
assert.ifError(err);
94+
assert.strictEqual(res.statusCode, 200);
95+
// check that the object data is the input stripped of the trailing checksum
96+
assert.strictEqual(res.body, objDataWithoutTrailingChecksum);
97+
return done();
98+
});
99+
});
100+
101+
itSkipIfAWS('should respond with BadRequest for signed trailing checksum', done => {
102+
const req = new HttpRequestAuthV4(
103+
`http://localhost:8000/${bucket}/${objectKey}`,
104+
Object.assign(
105+
{
106+
method: 'PUT',
107+
headers: {
108+
'content-length': objData.length,
109+
'x-amz-content-sha256': 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER',
110+
'x-amz-trailer': 'x-amz-checksum-sha256',
111+
},
112+
},
113+
authCredentials
114+
),
115+
res => {
116+
assert.strictEqual(res.statusCode, 400);
117+
res.on('data', () => {});
118+
res.on('end', done);
119+
}
120+
);
121+
122+
req.on('error', err => {
123+
assert.ifError(err);
124+
});
125+
126+
req.write(objData);
127+
128+
req.once('drain', () => {
129+
req.end();
130+
});
131+
});
132+
});

tests/functional/raw-node/test/unsupportedChecksums.js

Lines changed: 0 additions & 70 deletions
This file was deleted.

0 commit comments

Comments
 (0)