├── .eslintrc ├── package.json ├── README.md ├── LICENSE ├── .gitignore ├── index.js └── test └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | extends: "think" 3 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssrf-agent", 3 | "version": "1.0.5", 4 | "description": "prevent SSRF in http(s) request", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run lint && npm run test-cov", 8 | "test-cov": "nyc ava test/ && nyc report --reporter=html", 9 | "lint": "eslint --fix index.js", 10 | "prepublish": "npm test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/welefen/ssrf-agent.git" 15 | }, 16 | "author": "welefen", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/welefen/ssrf-agent/issues" 20 | }, 21 | "homepage": "https://github.com/welefen/ssrf-agent#readme", 22 | "devDependencies": { 23 | "ava": "^0.25.0", 24 | "eslint": "^5.4.0", 25 | "eslint-config-think": "^1.0.2", 26 | "node-fetch": "^2.2.0", 27 | "nyc": "^13.0.1" 28 | }, 29 | "dependencies": { 30 | "ip": "^1.1.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssrf-agent 2 | 3 | prevent SSRF in http(s) request 4 | 5 | ## Install 6 | 7 | ``` 8 | npm install ssrf-agent --save 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | const ssrfAgent = require('ssrf-agent'); 15 | const request = require('request'); 16 | // with request module 17 | const url = 'http://www.welefen.com' 18 | request(url, { 19 | agent: ssrfAgent(url) 20 | }, (err, response, body) => { 21 | 22 | }) 23 | ``` 24 | 25 | ```js 26 | const ssrfAgent = require('ssrf-agent'); 27 | const fetch = require('node-fetch'); 28 | // with node-fetch module 29 | const url = 'http://www.welefen.com' 30 | fetch(url, { 31 | agent: ssrfAgent(url) 32 | }).then(res => res.text).then(data => { 33 | 34 | }).catch(err => { 35 | 36 | }) 37 | ``` 38 | 39 | ## Options 40 | 41 | ```js 42 | const getAgent = require('ssrf-agent'); 43 | const agent = getAgent(ipChecker, agent); 44 | ``` 45 | * `ipChecker(ip)` {Function} check ip is allowed, default is `require('ip').isPrivate` 46 | * `agent` {String | Object} default is `http`, support `http` `https` or `agent instance` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Welefen Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | package-lock.json -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Agent: HttpAgent } = require('http'); 2 | const { Agent: HttpsAgent } = require('https'); 3 | const { isPrivate, isV4Format, isV6Format } = require('ip'); 4 | const httpAgent = new HttpAgent(); 5 | const httpsAgent = new HttpsAgent(); 6 | 7 | const getAgent = agent => { 8 | if (agent instanceof HttpAgent || agent instanceof HttpsAgent) return agent; 9 | if (agent.startsWith('https')) return httpsAgent; 10 | return httpAgent; 11 | }; 12 | 13 | const defaultIpChecker = ip => { 14 | if (isV4Format(ip) || isV6Format(ip)) { 15 | return !isPrivate(ip); 16 | } 17 | 18 | return true; 19 | }; 20 | 21 | const CREATE_CONNECTION = Symbol('createConnection'); 22 | 23 | module.exports = function( 24 | ipChecker = defaultIpChecker, 25 | agent = 'http' 26 | ) { 27 | if (typeof ipChecker === 'string') { 28 | agent = ipChecker; 29 | ipChecker = defaultIpChecker; 30 | } 31 | agent = getAgent(agent); 32 | if (agent[CREATE_CONNECTION]) return agent; 33 | agent[CREATE_CONNECTION] = true; 34 | 35 | const createConnection = agent.createConnection; 36 | agent.createConnection = function(options, fn) { 37 | const { host: address } = options; 38 | if (!ipChecker(address)) { 39 | throw new Error(`DNS lookup ${address} is not allowed.`); 40 | } 41 | 42 | const client = createConnection.call(this, options, fn); 43 | client.on('lookup', (err, address) => { 44 | if (err || ipChecker(address)) { 45 | return; 46 | } 47 | 48 | return client.destroy(new Error(`DNS lookup ${address} is not allowed.`)); 49 | }); 50 | 51 | return client; 52 | }; 53 | 54 | return agent; 55 | }; 56 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { test } = require('ava'); 2 | const { Agent: HTTPAgent } = require('http'); 3 | const { Agent: HTTPSAgent } = require('https'); 4 | const fetch = require('node-fetch'); 5 | const getAgent = require('../index'); 6 | 7 | test('getAgent with agent instance', async t => { 8 | const urls = [ 9 | 'http://127.0.0.1', 10 | 'https://www.baidu.com/' 11 | ]; 12 | 13 | t.plan(urls.length); 14 | 15 | try { 16 | await fetch(urls[0], { 17 | agent: getAgent(undefined, new HTTPAgent()) 18 | }); 19 | t.fail(); 20 | } catch (e) { 21 | t.pass(); 22 | } 23 | 24 | try { 25 | await fetch(urls[1], { 26 | agent: getAgent(undefined, new HTTPSAgent()) 27 | }); 28 | t.pass(); 29 | } catch (e) { 30 | t.fail(); 31 | } 32 | }); 33 | 34 | test('allowed url', async t => { 35 | const urls = [ 36 | 'https://www.welefen.com/', 37 | 'https://www.baidu.com/', 38 | 'https://www.so.com/?src=so.com', 39 | 'http://s0.qhres2.com/static/8f022693068c7a8c/fasdfasdf.js', 40 | 'https://www.so.com/s?ie=utf-8&fr=so.com&src=home_so.com&q=ww' 41 | ]; 42 | 43 | t.plan(urls.length); 44 | 45 | for (const item of urls) { 46 | const agent = getAgent(item); 47 | try { 48 | await fetch(item, { agent }); 49 | t.pass(); 50 | } catch (e) { 51 | t.fail(); 52 | } 53 | } 54 | }); 55 | 56 | test('disallowed url', async t => { 57 | const urls = [ 58 | 'http://017700000001', // ip host url with octonary number 59 | 'http://127.0.0.1.xip.io/', // url with local dns 60 | 'http://A.com@127.0.0.1', // url with @ 61 | 'http://127.0.0.1', // ip host url 62 | 'http://urlqh.cn/mgwC8', // short url with ip host url 63 | 'http://qiwoo.org/', // internal domain url 64 | 'http://urlqh.cn/meRow', // short url with internal domain url 65 | 'http://[::]:80/', 66 | 'http://0000::1:80/' 67 | ]; 68 | 69 | t.plan(urls.length); 70 | 71 | for (const item of urls) { 72 | const agent = getAgent(item); 73 | try { 74 | await fetch(item, { agent }); 75 | t.fail(); 76 | } catch (e) { 77 | t.pass(); 78 | } 79 | } 80 | }); 81 | --------------------------------------------------------------------------------