Skip to content

Commit 0d8ede6

Browse files
Merge pull request #8065 from BitGo/CAAS-659
feat: capture raw body bytes for v4 hmac calculation
2 parents 20c2a51 + 2019b7a commit 0d8ede6

File tree

6 files changed

+617
-25
lines changed

6 files changed

+617
-25
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,38 @@ async function handleNetworkV1EnterpriseClientConnections(
11931193
return handleProxyReq(req, res, next);
11941194
}
11951195

1196+
/**
1197+
* Helper to send request body, using raw bytes when available.
1198+
*
1199+
* For v4 HMAC authentication, we need to send the exact bytes that were
1200+
* received from the client to ensure the HMAC signature matches.
1201+
* The rawBodyBuffer is captured by body-parser's verify callback before
1202+
* JSON parsing, preserving exact whitespace, key ordering, etc.
1203+
*
1204+
* For v2/v3, sending the raw string also works because serializeRequestData
1205+
* now properly returns strings as-is for HMAC calculation.
1206+
*
1207+
* @param request - The superagent request object
1208+
* @param req - The Express request containing body and rawBodyBuffer
1209+
* @returns The request with body attached
1210+
*/
1211+
function sendRequestBody(request: ReturnType<BitGo['post']>, req: express.Request) {
1212+
if (req.rawBodyBuffer) {
1213+
// Preserve original Content-Type header from client
1214+
const contentTypeHeader = req.headers['content-type'];
1215+
if (contentTypeHeader) {
1216+
request.set('Content-Type', Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader);
1217+
}
1218+
// Send raw body as UTF-8 string to preserve exact bytes for HMAC.
1219+
// JSON is always UTF-8 (RFC 8259), so this is lossless for JSON bodies.
1220+
// serializeRequestData will return this string as-is for HMAC calculation.
1221+
return request.send(req.rawBodyBuffer.toString('utf8'));
1222+
}
1223+
1224+
// Fall back to parsed body for backward compatibility (e.g., non-JSON bodies)
1225+
return request.send(req.body);
1226+
}
1227+
11961228
/**
11971229
* Redirect a request using the bitgo request functions.
11981230
* @param bitgo
@@ -1215,19 +1247,19 @@ export function redirectRequest(
12151247
request = bitgo.get(url);
12161248
break;
12171249
case 'POST':
1218-
request = bitgo.post(url).send(req.body);
1250+
request = sendRequestBody(bitgo.post(url), req);
12191251
break;
12201252
case 'PUT':
1221-
request = bitgo.put(url).send(req.body);
1253+
request = sendRequestBody(bitgo.put(url), req);
12221254
break;
12231255
case 'PATCH':
1224-
request = bitgo.patch(url).send(req.body);
1256+
request = sendRequestBody(bitgo.patch(url), req);
12251257
break;
12261258
case 'OPTIONS':
1227-
request = bitgo.options(url).send(req.body);
1259+
request = sendRequestBody(bitgo.options(url), req);
12281260
break;
12291261
case 'DELETE':
1230-
request = bitgo.del(url).send(req.body);
1262+
request = sendRequestBody(bitgo.del(url), req);
12311263
break;
12321264
}
12331265

@@ -1268,7 +1300,12 @@ function apiResponse(status: number, result: any, message?: string): ApiResponse
12681300
return new ApiResponseError(message, status, result);
12691301
}
12701302

1271-
const expressJSONParser = bodyParser.json({ limit: '20mb' });
1303+
const expressJSONParser = bodyParser.json({
1304+
limit: '20mb',
1305+
verify: (req, res, buf) => {
1306+
(req as express.Request).rawBodyBuffer = buf;
1307+
},
1308+
});
12721309

12731310
/**
12741311
* Perform body parsing here only on routes we want

modules/express/src/expressApp.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,18 @@ export function app(cfg: Config): express.Application {
302302
checkPreconditions(cfg);
303303
debug('preconditions satisfied');
304304

305-
app.use(bodyParser.json({ limit: '20mb' }));
305+
app.use(
306+
bodyParser.json({
307+
limit: '20mb',
308+
verify: (req, res, buf) => {
309+
// Store the raw body buffer on the request object.
310+
// This preserves the exact bytes before JSON parsing,
311+
// which may alter whitespace, key ordering, etc.
312+
// Required for v4 HMAC authentication.
313+
(req as express.Request).rawBodyBuffer = buf;
314+
},
315+
})
316+
);
306317

307318
// Be more robust about accepting URLs with double slashes
308319
app.use(function replaceUrlSlashes(req, res, next) {

0 commit comments

Comments
 (0)