Skip to content

Commit

Permalink
feat(#2264): add more robust header validation for inbound server (#278)
Browse files Browse the repository at this point in the history
* feat: add more robust header validation for inbound server

* chore: fix comment

* chore: add error test cases

* chore: fix tests and linting

* chore: fix test description

* chore: remove unnecessary duplicated code

* chore: install central-services-shared

* chore: fix packages

* chore: resolve vulnerabilites
  • Loading branch information
kleyow authored Jun 21, 2021
1 parent 47844e5 commit 9ea24d7
Show file tree
Hide file tree
Showing 9 changed files with 5,128 additions and 1,748 deletions.
132 changes: 108 additions & 24 deletions src/InboundServer/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ const coBody = require('co-body');

const randomPhrase = require('../lib/randomphrase');
const { Jws, Errors } = require('@mojaloop/sdk-standard-components');
const {
parseAcceptHeader,
parseContentTypeHeader,
protocolVersionsMap
} = require('@mojaloop/central-services-shared').Util.HeaderValidation;
const {
defaultProtocolResources,
defaultProtocolVersions,
errorMessages
} = require('@mojaloop/central-services-shared').Util.Hapi.FSPIOPHeaderValidation;

/**
* Log raw to console as a last resort
Expand Down Expand Up @@ -154,38 +164,112 @@ const createRequestIdGenerator = () => async (ctx, next) => {

/**
* Deal with mojaloop API content type headers, treat as JSON
* This is based on the Hapi header validation plugin found in `central-services-shared`
* Since `sdk-scheme-adapter` uses Koa instead of Hapi we convert the plugin
* into middleware
* @param logger
* @return {Function}
*/
//
const createHeaderValidator = (logger) => async (ctx, next) => {
const validHeaders = new Set([
'application/vnd.interoperability.parties+json;version=1.0',
'application/vnd.interoperability.parties+json;version=1.1',
'application/vnd.interoperability.participants+json;version=1.0',
'application/vnd.interoperability.quotes+json;version=1.0',
'application/vnd.interoperability.quotes+json;version=1.1',
'application/vnd.interoperability.bulkQuotes+json;version=1.0',
'application/vnd.interoperability.bulkQuotes+json;version=1.1',
'application/vnd.interoperability.bulkTransfers+json;version=1.0',
'application/vnd.interoperability.transactionRequests+json;version=1.0',
'application/vnd.interoperability.transfers+json;version=1.0',
'application/vnd.interoperability.transfers+json;version=1.1',
'application/vnd.interoperability.authorizations+json;version=1.0',
'application/json'
]);
if (validHeaders.has(ctx.request.headers['content-type'])) {
try {
ctx.request.body = await coBody.json(ctx.req);
const createHeaderValidator = (logger) => async (
ctx,
next,
resources = defaultProtocolResources,
supportedProtocolVersions = defaultProtocolVersions
) => {
const request = ctx.request;

// First, extract the resource type from the path
const resource = request.path.replace(/^\//, '').split('/')[0];

// Only validate requests for the requested resources
if (!resources.includes(resource)) {
await next();
}

// Always validate the accept header for a get request, or optionally if it has been
// supplied
if (request.method.toLowerCase() === 'get' || request.headers.accept) {
if (request.headers.accept === undefined) {
ctx.response.status = Errors.MojaloopApiErrorCodes.MISSING_ELEMENT.httpStatusCode;
ctx.response.body = new Errors.MojaloopFSPIOPError(
Errors.MojaloopApiErrorObjectFromCode(Errors.MojaloopApiErrorCodes.MISSING_ELEMENT.httpStatusCode),
errorMessages.REQUIRE_ACCEPT_HEADER,
null,
Errors.MojaloopApiErrorCodes.MISSING_ELEMENT
).toApiErrorObject();
return;
}
catch(err) {
// error parsing body
logger.push({ err }).log('Error parsing body');
const accept = parseAcceptHeader(resource, request.headers.accept);
if (!accept.valid) {
ctx.response.status = Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX.httpStatusCode;
ctx.response.body = new Errors.MojaloopFSPIOPError(err, err.message, null,
Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX).toApiErrorObject();
ctx.response.body = new Errors.MojaloopFSPIOPError(
Errors.MojaloopApiErrorObjectFromCode(Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX.httpStatusCode),
errorMessages.INVALID_ACCEPT_HEADER,
null,
Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX
).toApiErrorObject();
return;
}
if (!supportedProtocolVersions.some(supportedVer => accept.versions.has(supportedVer))) {
ctx.response.status = Errors.MojaloopApiErrorCodes.UNACCEPTABLE_VERSION.httpStatusCode;
ctx.response.body = new Errors.MojaloopFSPIOPError(
Errors.MojaloopApiErrorObjectFromCode(Errors.MojaloopApiErrorCodes.UNACCEPTABLE_VERSION.httpStatusCode),
errorMessages.REQUESTED_VERSION_NOT_SUPPORTED,
null,
Errors.MojaloopApiErrorCodes.UNACCEPTABLE_VERSION,
protocolVersionsMap
).toApiErrorObject();
return;
}
}

// Always validate the content-type header
if (request.headers['content-type'] === undefined) {
ctx.response.status = Errors.MojaloopApiErrorCodes.MISSING_ELEMENT.httpStatusCode;
ctx.response.body = new Errors.MojaloopFSPIOPError(
Errors.MojaloopApiErrorObjectFromCode(Errors.MojaloopApiErrorCodes.MISSING_ELEMENT.httpStatusCode),
errorMessages.REQUIRE_CONTENT_TYPE_HEADER,
null,
Errors.MojaloopApiErrorCodes.MISSING_ELEMENT,
protocolVersionsMap
).toApiErrorObject();
return;
}

const contentType = parseContentTypeHeader(resource, request.headers['content-type']);
if (!contentType.valid) {
ctx.response.status = Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX.httpStatusCode;
ctx.response.body = new Errors.MojaloopFSPIOPError(
Errors.MojaloopApiErrorObjectFromCode(Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX.httpStatusCode),
errorMessages.INVALID_CONTENT_TYPE_HEADER,
null,
Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX
).toApiErrorObject();
return;
}
if (!supportedProtocolVersions.includes(contentType.version)) {
ctx.response.status = Errors.MojaloopApiErrorCodes.UNACCEPTABLE_VERSION.httpStatusCode;
ctx.response.body = new Errors.MojaloopFSPIOPError(
Errors.MojaloopApiErrorObjectFromCode(Errors.MojaloopApiErrorCodes.UNACCEPTABLE_VERSION.httpStatusCode),
errorMessages.SUPPLIED_VERSION_NOT_SUPPORTED,
null,
Errors.MojaloopApiErrorCodes.UNACCEPTABLE_VERSION,
protocolVersionsMap
).toApiErrorObject();
return;
}

try {
ctx.request.body = await coBody.json(ctx.req);
}
catch(err) {
// error parsing body
logger.push({ err }).log('Error parsing body');
ctx.response.status = Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX.httpStatusCode;
ctx.response.body = new Errors.MojaloopFSPIOPError(err, err.message, null,
Errors.MojaloopApiErrorCodes.MALFORMED_SYNTAX).toApiErrorObject();
return;
}
await next();
};
Expand Down
49 changes: 49 additions & 0 deletions src/audit-resolve.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"decisions": {
"1556|@mojaloop/central-services-shared>widdershins>node-fetch": {
"decision": "fix",
"madeAt": 1624032509533
},
"1589|@mojaloop/central-services-shared>rc>ini": {
"decision": "fix",
"madeAt": 1624032519962
},
"1654|@mojaloop/central-services-shared>widdershins>swagger2openapi>yargs>y18n": {
"decision": "fix",
"madeAt": 1624032531602
},
"1654|@mojaloop/central-services-shared>widdershins>yargs>y18n": {
"decision": "fix",
"madeAt": 1624032531602
},
"1673|@mojaloop/central-services-shared>openapi-backend>lodash": {
"decision": "fix",
"madeAt": 1624032541954
},
"1673|@mojaloop/central-services-shared>shins>sanitize-html>lodash": {
"decision": "fix",
"madeAt": 1624032541954
},
"1673|koa2-oauth-server>00unidentified>lodash": {
"decision": "fix",
"madeAt": 1624032544849
},
"1500|@mojaloop/central-services-shared>widdershins>yargs>yargs-parser": {
"decision": "ignore",
"madeAt": 1624032547741,
"expiresAt": 1626624496536
},
"1675|@mojaloop/central-services-shared>shins>sanitize-html": {
"decision": "ignore",
"madeAt": 1624032548905,
"expiresAt": 1626624496536
},
"1676|@mojaloop/central-services-shared>shins>sanitize-html": {
"decision": "ignore",
"madeAt": 1624032548905,
"expiresAt": 1626624496536
}
},
"rules": {},
"version": 1
}
Loading

0 comments on commit 9ea24d7

Please sign in to comment.