Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,38 @@ async function handleNetworkV1EnterpriseClientConnections(
return handleProxyReq(req, res, next);
}

/**
* Helper to send request body, using raw bytes when available.
*
* For v4 HMAC authentication, we need to send the exact bytes that were
* received from the client to ensure the HMAC signature matches.
* The rawBodyBuffer is captured by body-parser's verify callback before
* JSON parsing, preserving exact whitespace, key ordering, etc.
*
* For v2/v3, sending the raw string also works because serializeRequestData
* now properly returns strings as-is for HMAC calculation.
*
* @param request - The superagent request object
* @param req - The Express request containing body and rawBodyBuffer
* @returns The request with body attached
*/
function sendRequestBody(request: ReturnType<BitGo['post']>, req: express.Request) {
if (req.rawBodyBuffer) {
// Preserve original Content-Type header from client
const contentTypeHeader = req.headers['content-type'];
if (contentTypeHeader) {
request.set('Content-Type', Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader);
}
// Send raw body as UTF-8 string to preserve exact bytes for HMAC.
// JSON is always UTF-8 (RFC 8259), so this is lossless for JSON bodies.
// serializeRequestData will return this string as-is for HMAC calculation.
return request.send(req.rawBodyBuffer.toString('utf8'));
}

// Fall back to parsed body for backward compatibility (e.g., non-JSON bodies)
return request.send(req.body);
}

/**
* Redirect a request using the bitgo request functions.
* @param bitgo
Expand All @@ -1214,19 +1246,19 @@ export function redirectRequest(
request = bitgo.get(url);
break;
case 'POST':
request = bitgo.post(url).send(req.body);
request = sendRequestBody(bitgo.post(url), req);
break;
case 'PUT':
request = bitgo.put(url).send(req.body);
request = sendRequestBody(bitgo.put(url), req);
break;
case 'PATCH':
request = bitgo.patch(url).send(req.body);
request = sendRequestBody(bitgo.patch(url), req);
break;
case 'OPTIONS':
request = bitgo.options(url).send(req.body);
request = sendRequestBody(bitgo.options(url), req);
break;
case 'DELETE':
request = bitgo.del(url).send(req.body);
request = sendRequestBody(bitgo.del(url), req);
break;
}

Expand Down Expand Up @@ -1267,7 +1299,12 @@ function apiResponse(status: number, result: any, message?: string): ApiResponse
return new ApiResponseError(message, status, result);
}

const expressJSONParser = bodyParser.json({ limit: '20mb' });
const expressJSONParser = bodyParser.json({
limit: '20mb',
verify: (req, res, buf) => {
(req as express.Request).rawBodyBuffer = buf;
},
});

/**
* Perform body parsing here only on routes we want
Expand Down
13 changes: 12 additions & 1 deletion modules/express/src/expressApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,18 @@ export function app(cfg: Config): express.Application {
checkPreconditions(cfg);
debug('preconditions satisfied');

app.use(bodyParser.json({ limit: '20mb' }));
app.use(
bodyParser.json({
limit: '20mb',
verify: (req, res, buf) => {
// Store the raw body buffer on the request object.
// This preserves the exact bytes before JSON parsing,
// which may alter whitespace, key ordering, etc.
// Required for v4 HMAC authentication.
(req as express.Request).rawBodyBuffer = buf;
},
})
);

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