From 92a34f3246ded7ebb5c628d1de2a37a121ce919d Mon Sep 17 00:00:00 2001 From: killa Date: Mon, 8 Jul 2024 23:02:41 +0800 Subject: [PATCH] feat: add hostnameExceptionList for ssrf (#100) ## Summary by CodeRabbit - **New Features** - Introduced `hostnameExceptionList` for specifying allowed hostnames within IP blacklists, enhancing SSRF protection. - **Bug Fixes** - Adjusted access method for `refererWhiteList` to improve security configuration handling. - **Tests** - Added test cases to validate the functionality of the new `hostnameExceptionList` feature. - **Documentation** - Updated README to include information about the new `hostnameExceptionList` in security configurations. --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- README.md | 5 +++ config/config.default.js | 1 + lib/utils.js | 9 +++++- .../config/config.default.js | 15 +++++++++ .../ssrf-hostname-exception-list/package.json | 3 ++ test/ssrf.test.js | 31 +++++++++++++++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/apps/ssrf-hostname-exception-list/config/config.default.js create mode 100644 test/fixtures/apps/ssrf-hostname-exception-list/package.json diff --git a/README.md b/README.md index 74d5e21..b921d2e 100644 --- a/README.md +++ b/README.md @@ -523,6 +523,7 @@ In a [Server-Side Request Forgery (SSRF)](https://www.owasp.org/index.php/Server - ipBlackList(Array) - specific which IP addresses are illegal when requested with `safeCurl`. - ipExceptionList(Array) - specific which IP addresses are legal within ipBlackList. +hostnameExceptionList(Array) - specifies which hostnames are legal within ipBlackList. - checkAddress(Function) - determine the ip by the function's return value, `false` means illegal ip. ```js @@ -540,6 +541,10 @@ exports.security = { '10.1.1.1', '10.10.0.1/24', ], + // legal hostname + hostnameExceptionList: [ + 'example.com', + ], // checkAddress has higher priority than ipBlackList checkAddress(ip) { return ip !== '127.0.0.1'; diff --git a/config/config.default.js b/config/config.default.js index 508d01a..e6e4520 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -103,6 +103,7 @@ module.exports = () => { ssrf: { ipBlackList: null, ipExceptionList: null, + hostnameExceptionList: null, checkAddress: null, }, }; diff --git a/lib/utils.js b/lib/utils.js index e451406..c013cf1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -111,7 +111,14 @@ exports.preprocessConfig = function(config) { if (ssrf && ssrf.ipBlackList && !ssrf.checkAddress) { const containsList = ssrf.ipBlackList.map(getContains); const exceptionList = (ssrf.ipExceptionList || []).map(getContains); - ssrf.checkAddress = ipAddresses => { + const hostnameExceptionList = ssrf.hostnameExceptionList; + ssrf.checkAddress = (ipAddresses, family, hostname) => { + // Check hostname first + if (hostname && hostnameExceptionList) { + if (hostnameExceptionList.includes(hostname)) { + return true; + } + } // ipAddresses will be array address on Node.js >= 20 // [ // { address: '220.181.125.241', family: 4 }, diff --git a/test/fixtures/apps/ssrf-hostname-exception-list/config/config.default.js b/test/fixtures/apps/ssrf-hostname-exception-list/config/config.default.js new file mode 100644 index 0000000..f88cc14 --- /dev/null +++ b/test/fixtures/apps/ssrf-hostname-exception-list/config/config.default.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.security = { + ssrf: { + ipBlackList: [ + '10.0.0.0/8', + '127.0.0.1', + '0.0.0.0/32', + ], + hostnameExceptionList: [ + 'registry.npmjs.org', + 'registry.npmmirror.com', + ], + }, +}; diff --git a/test/fixtures/apps/ssrf-hostname-exception-list/package.json b/test/fixtures/apps/ssrf-hostname-exception-list/package.json new file mode 100644 index 0000000..868fb2c --- /dev/null +++ b/test/fixtures/apps/ssrf-hostname-exception-list/package.json @@ -0,0 +1,3 @@ +{ + "name": "ssrf-ip-black-list" +} \ No newline at end of file diff --git a/test/ssrf.test.js b/test/ssrf.test.js index 2840731..dbe5277 100644 --- a/test/ssrf.test.js +++ b/test/ssrf.test.js @@ -171,6 +171,37 @@ describe('test/ssrf.test.js', () => { assert(count === 3); }); }); + + describe('hostnameExceptionList', () => { + before(() => { + app = mm.app({ baseDir: 'apps/ssrf-hostname-exception-list' }); + return app.ready(); + }); + + it('should safeCurl work', async () => { + const ctx = app.createAnonymousContext(); + const host = process.env.CI ? 'registry.npmjs.org' : 'registry.npmmirror.com'; + const url = `https://${host}`; + let count = 0; + + mm(app, 'curl', async (url, options) => { + options.checkAddress('10.0.0.1', 4, host) && count++; + return 'response'; + }); + mm(app.agent, 'curl', async (url, options) => { + options.checkAddress('10.0.0.1', 4, host) && count++; + return 'response'; + }); + mm(ctx, 'curl', async (url, options) => { + options.checkAddress('10.0.0.1', 4, host) && count++; + return 'response'; + }); + + await app.safeCurl(url, { dataType: 'json' }); + await app.agent.safeCurl(url, { dataType: 'json' }); + await ctx.safeCurl(url, { dataType: 'json' }); + }); + }); }); async function checkIllegalAddressError(instance, url) {