├── .gitignore ├── Gruntfile.js ├── CHANGELOG.md ├── package.json ├── LICENSE ├── index.js ├── README.md └── test └── express-sslify.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.loadNpmTasks('grunt-mocha-test'); 4 | 5 | grunt.initConfig({ 6 | mochaTest: { 7 | test: { 8 | options: { 9 | reporter: 'spec' 10 | }, 11 | src: ['test/**/*.test.js'] 12 | } 13 | } 14 | }); 15 | 16 | grunt.registerTask('test', ['mochaTest']); 17 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.2.0 - Dec 22 2016 2 | - ADD Support for 'x-forwarded-host' headers to indicate to which host requests should be redirected 3 | 4 | # 1.1.0 - Jul 02 2016 5 | - ADD Support for 'x-forwarded-proto' headers that were appended by several proxies in the format 'https, http' 6 | - UPDATE Tests for Azure HTTPS to have more realistic content for the x-arr-ssl flag 7 | - UPDATE JSDoc and Readme 8 | 9 | # 1.0.1 - Oct 25 2015 10 | - ADD redirects for HEAD requests 11 | 12 | # 1.0.0 - Oct 24 2015 13 | - BREAKING change of arguments to named arg. - check README 14 | - UPDATE documentation 15 | 16 | # 0.1.2 - Jul 06 2015 17 | - UPDATE dependencies 18 | - BUMP package version 19 | 20 | # 0.1.1 - Nov 15 2014 21 | - ADDED Azure proxy support 22 | - ADDED Testing 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-sslify", 3 | "version": "1.2.0", 4 | "description": "Enforces SSL for node.js express projects", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "grunt test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/florianheinemann/express-sslify.git" 12 | }, 13 | "keywords": [ 14 | "express", 15 | "node.js", 16 | "node", 17 | "ssl", 18 | "sslify", 19 | "http", 20 | "redirect" 21 | ], 22 | "author": "Florian Heinemann (http://twitter.com/florian__h)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/florianheinemann/express-sslify/issues" 26 | }, 27 | "homepage": "https://github.com/florianheinemann/express-sslify", 28 | "devDependencies": { 29 | "chai": "^3.0.0", 30 | "express": "^4.13.1", 31 | "grunt": "^1.0.1", 32 | "grunt-mocha-test": "^0.12.7", 33 | "mocha": "^2.2.5", 34 | "supertest": "^1.0.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Florian Heinemann 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. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var defaults = { 4 | trustProtoHeader: false, 5 | trustAzureHeader: false, 6 | trustXForwardedHostHeader: false 7 | }; 8 | 9 | /** 10 | * Apply options 11 | * 12 | * @param {Hash} [options] 13 | * @return {Hash} 14 | * @api private 15 | */ 16 | function applyOptions(options) { 17 | var settings = {}; 18 | options = options || {}; 19 | 20 | for (var option in defaults) { 21 | settings[option] = options[option] || defaults[option]; 22 | } 23 | return settings; 24 | } 25 | 26 | /** 27 | * enforceHTTPS 28 | * 29 | * @param {Hash} [options] 30 | * @param {Boolean} [options[trustProtoHeader]=false] - Set to true if the x-forwarded-proto HTTP header should be trusted (e.g. for typical reverse proxy configurations) 31 | * @param {Boolean} [options[trustAzureHeader]=false] - Set to true if Azure's x-arr-ssl HTTP header should be trusted (only use in Azure environments) 32 | * @param {Boolean} [options[trustXForwardedHostHeader]=false] - Set to true if the x-forwarded-host HTTP header should be trusted 33 | * @api public 34 | */ 35 | var enforceHTTPS = function(options) { 36 | return function(req, res, next) { 37 | // Crash on pre-1.0.0-style arguments 38 | if(typeof options === 'boolean') { 39 | return next("express-sslify has changed the way how arguments are treated. Please check the readme."); 40 | } 41 | 42 | options = applyOptions(options); 43 | 44 | // First, check if directly requested via https 45 | var isHttps = req.secure; 46 | 47 | // Second, if the request headers can be trusted (e.g. because they are send 48 | // by a proxy), check if x-forward-proto is set to https 49 | if(!isHttps && options.trustProtoHeader) { 50 | isHttps = ((req.headers["x-forwarded-proto"] || '').substring(0,5) === 'https'); 51 | } 52 | 53 | // Third, if trustAzureHeader is set, check for Azure's headers 54 | // indicating a SSL connection 55 | if(!isHttps && options.trustAzureHeader && req.headers["x-arr-ssl"]) { 56 | isHttps = true; 57 | } 58 | 59 | if(isHttps) { 60 | next(); 61 | } else { 62 | // Only redirect GET methods 63 | if(req.method === "GET" || req.method === 'HEAD') { 64 | var host = options.trustXForwardedHostHeader ? (req.headers['x-forwarded-host'] || req.headers.host) : req.headers.host; 65 | res.redirect(301, "https://" + host + req.originalUrl); 66 | } else { 67 | res.status(403).send("Please use HTTPS when submitting data to this server."); 68 | } 69 | } 70 | }; 71 | }; 72 | 73 | exports.HTTPS = enforceHTTPS; 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | express-sslify 2 | ============== 3 | 4 | This simple module enforces HTTPS connections on any incoming GET and HEAD requests. In case of a non-encrypted HTTP request, express-sslify automatically redirects to an HTTPS address using a 301 permanent redirect. Any other type of request (e.g., POST) will fail with a 403 error message. 5 | 6 | express-sslify also works behind reverse proxies (load balancers) such as those used by Heroku or nodejitsu. In those cases, however, the `trustProtoHeader` parameter has to be set (see below) 7 | 8 | ### Usage 9 | 10 | First, install the module: 11 | 12 | `$ npm install express-sslify --save` 13 | 14 | Afterwards, require the module and *use* the `HTTPS()` method: 15 | ```javascript 16 | var express = require('express'); 17 | var http = require('http'); 18 | var enforce = require('express-sslify'); 19 | 20 | var app = express(); 21 | 22 | // Use enforce.HTTPS({ trustProtoHeader: true }) in case you are behind 23 | // a load balancer (e.g. Heroku). See further comments below 24 | app.use(enforce.HTTPS()); 25 | 26 | http.createServer(app).listen(app.get('port'), function() { 27 | console.log('Express server listening on port ' + app.get('port')); 28 | }); 29 | ``` 30 | 31 | ### Reverse Proxies (Heroku, nodejitsu and others) 32 | 33 | Heroku, nodejitsu and other hosters often use reverse proxies which offer SSL endpoints but then forward unencrypted HTTP traffic to the website. This makes it difficult to detect if the original request was indeed via HTTPS. Luckily, most reverse proxies set the `x-forwarded-proto` header flag with the original request scheme. express-sslify is ready for such scenarios, but you have to specifically request the evaluation of this flag: 34 | 35 | ```javascript 36 | app.use(enforce.HTTPS({ trustProtoHeader: true })) 37 | ``` 38 | 39 | Please do *not* set this flag if you are not behind a proxy that is setting this flag. HTTP headers can be easily spoofed outside of environments that are actively setting/removing the header. 40 | 41 | ### Azure support 42 | 43 | Azure has a slightly different way of signaling encrypted connections. To tell express-sslify to look out for Azure's x-arr-ssl header do the following: 44 | 45 | ```javascript 46 | app.use(enforce.HTTPS({ trustAzureHeader: true })) 47 | ``` 48 | 49 | Please do *not* set this flag if you are not behind an Azure proxy as this flag can be easily spoofed outside of an Azure environment. 50 | 51 | ### X-Forwarded-Host header support 52 | 53 | If your reverse proxy sends the original host using the `X-Forwarded-Host` header and you need to use that instead of the `Host` header for the redirect, use the `trustXForwardedHostHeader` flag: 54 | 55 | ```javascript 56 | app.use(enforce.HTTPS({ trustXForwardedHostHeader: true })) 57 | ``` 58 | 59 | ## Tests 60 | Download the whole repository and call: 61 | ```shell 62 | $ npm install; npm test 63 | ``` 64 | 65 | ### Credits and License 66 | express-sslify is licensed under the MIT license. If you'd like to be informed about new projects follow [@TheSumOfAll](http://twitter.com/TheSumOfAll/). 67 | 68 | Copyright (c) 2013-2016 Florian Heinemann 69 | -------------------------------------------------------------------------------- /test/express-sslify.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var express = require('express'); 5 | var request = require('supertest'); 6 | var enforce = require('../index.js'); 7 | 8 | describe('express-sslify', function() { 9 | describe('HTTPS not enforced', function() { 10 | 11 | var app = express(); 12 | 13 | app.get('/non-ssl', 14 | function(req, res){ 15 | res.status(200).send('ok'); 16 | }); 17 | 18 | app.head('/non-ssl-head', 19 | function(req, res){ 20 | res.status(200).send(); 21 | }); 22 | 23 | app.post('/non-ssl-post', 24 | function(req, res){ 25 | res.status(200).send('ok'); 26 | }); 27 | 28 | var agent = request.agent(app); 29 | 30 | it('should accept non-ssl requests', function (done) { 31 | agent 32 | .get('/non-ssl') 33 | .expect(200, 'ok', done); 34 | }) 35 | 36 | it('should accept non-ssl HEAD requests', function (done) { 37 | agent 38 | .head('/non-ssl-head') 39 | .expect(200, done); 40 | }) 41 | 42 | it('should accept non-ssl POST requests', function (done) { 43 | agent 44 | .post('/non-ssl-post') 45 | .expect(200, 'ok', done); 46 | }) 47 | }) 48 | 49 | describe('HTTPS enforced', function() { 50 | 51 | var app = express(); 52 | 53 | app.use(enforce.HTTPS()); 54 | 55 | app.get('/ssl', 56 | function(req, res){ 57 | res.status(200).send('ok'); 58 | }); 59 | 60 | app.head('/ssl-head', 61 | function(req, res){ 62 | res.status(200).send(); 63 | }); 64 | 65 | app.post('/ssl-post', 66 | function(req, res){ 67 | res.status(200).send('ok'); 68 | }); 69 | 70 | var agent = request.agent(app); 71 | 72 | it('should redirect non-SSL GET requests to HTTPS', function (done) { 73 | agent 74 | .get('/ssl') 75 | .expect(301) 76 | .expect('location', new RegExp('^https://[\\S]*/ssl$'), done); 77 | }) 78 | 79 | it('should redirect non-SSL HEAD requests to HTTPS', function (done) { 80 | agent 81 | .head('/ssl-head') 82 | .expect(301) 83 | .expect('location', new RegExp('^https://[\\S]*/ssl-head$'), done); 84 | }) 85 | 86 | it('should send error for non-SSL POST requests', function (done) { 87 | agent 88 | .post('/non-ssl-post') 89 | .expect(403, done); 90 | }) 91 | }) 92 | 93 | describe('Heroku-style proxy SSL flag', function() { 94 | 95 | var proxyTests = function(method) { 96 | 97 | var app = express(); 98 | 99 | app[method]('/ssl', enforce.HTTPS(), 100 | function(req, res){ 101 | res.status(200).send(); 102 | }); 103 | 104 | app[method]('/ssl-behind-proxy', enforce.HTTPS({ trustProtoHeader: true }), 105 | function(req, res){ 106 | res.status(200).send(); 107 | }); 108 | 109 | var agent = request.agent(app); 110 | 111 | it('should ignore x-forwarded-proto if not activated (' + method.toUpperCase() + ')', function (done) { 112 | agent 113 | [method]('/ssl') 114 | .set('x-forwarded-proto', 'https') 115 | .expect(301) 116 | .expect('location', new RegExp('^https://[\\S]*/ssl$'), done); 117 | }) 118 | 119 | it('should accept request if flag set and activated (' + method.toUpperCase() + ')', function (done) { 120 | agent 121 | [method]('/ssl-behind-proxy') 122 | .set('x-forwarded-proto', 'https') 123 | .expect(200, done); 124 | }) 125 | 126 | it('should accept request if flag set and activated (' + method.toUpperCase() + ') with comma/space separated list', function (done) { 127 | agent 128 | [method]('/ssl-behind-proxy') 129 | .set('x-forwarded-proto', 'https, http') 130 | .expect(200, done); 131 | }) 132 | 133 | it('should accept request if flag set and activated (' + method.toUpperCase() + ') with comma separated list', function (done) { 134 | agent 135 | [method]('/ssl-behind-proxy') 136 | .set('x-forwarded-proto', 'https,http') 137 | .expect(200, done); 138 | }) 139 | 140 | it('should redirect if activated but flag not set with https (' + method.toUpperCase() + ')', function (done) { 141 | agent 142 | [method]('/ssl-behind-proxy') 143 | .set('x-forwarded-proto', '') 144 | .expect(301) 145 | .expect('location', new RegExp('^https://[\\S]*/ssl-behind-proxy$'), done); 146 | }) 147 | 148 | it('should redirect if activated but flag only set with HTTP (' + method.toUpperCase() + ')', function (done) { 149 | agent 150 | [method]('/ssl-behind-proxy') 151 | .set('x-forwarded-proto', 'http') 152 | .expect(301) 153 | .expect('location', new RegExp('^https://[\\S]*/ssl-behind-proxy$'), done); 154 | }) 155 | 156 | it('should redirect if activated but header indicates that first hop was not HTTPS (' + method.toUpperCase() + ')', function (done) { 157 | agent 158 | [method]('/ssl-behind-proxy') 159 | .set('x-forwarded-proto', 'http, https') 160 | .expect(301) 161 | .expect('location', new RegExp('^https://[\\S]*/ssl-behind-proxy$'), done); 162 | }) 163 | 164 | it('should redirect if activated but flag not set (' + method.toUpperCase() + ')', function (done) { 165 | agent 166 | [method]('/ssl-behind-proxy') 167 | .expect(301) 168 | .expect('location', new RegExp('^https://[\\S]*/ssl-behind-proxy$'), done); 169 | }) 170 | 171 | it('should redirect if activated but wrong flag set (' + method.toUpperCase() + ')', function (done) { 172 | agent 173 | [method]('/ssl-behind-proxy') 174 | .set('x-arr-ssl', 'https') 175 | .expect(301) 176 | .expect('location', new RegExp('^https://[\\S]*/ssl-behind-proxy$'), done); 177 | }) 178 | } 179 | 180 | // Test GET requests 181 | proxyTests('get'); 182 | 183 | // Test HEAD requests 184 | proxyTests('head'); 185 | }) 186 | 187 | describe('Azure-style proxy SSL flag', function() { 188 | 189 | var proxyTests = function(method) { 190 | 191 | var app = express(); 192 | 193 | var xArrSslContent = '2048|128|DC=com, DC=microsoft, DC=corp, DC=redmond, CN=MSIT Machine Auth CA 2|C=US, S=WA, L=Redmond, O=Microsoft, OU=OrganizationName, CN=*.azurewebsites.net'; 194 | 195 | app[method]('/ssl', enforce.HTTPS(), 196 | function(req, res){ 197 | res.status(200).send(); 198 | }); 199 | 200 | app[method]('/ssl-behind-azure', enforce.HTTPS({trustAzureHeader: true}), 201 | function(req, res){ 202 | res.status(200).send(); 203 | }); 204 | 205 | var agent = request.agent(app); 206 | 207 | it('should ignore x-arr-ssl if not activated (' + method.toUpperCase() + ')', function (done) { 208 | agent 209 | [method]('/ssl') 210 | .set('x-arr-ssl', xArrSslContent) 211 | .expect(301) 212 | .expect('location', new RegExp('^https://[\\S]*/ssl$'), done); 213 | }) 214 | 215 | it('should accept request if flag set and activated (' + method.toUpperCase() + ')', function (done) { 216 | agent 217 | [method]('/ssl-behind-azure') 218 | .set('x-arr-ssl', xArrSslContent) 219 | .expect(200, done); 220 | }) 221 | 222 | it('should redirect if activated but flag not set (' + method.toUpperCase() + ')', function (done) { 223 | agent 224 | [method]('/ssl-behind-azure') 225 | .expect(301) 226 | .expect('location', new RegExp('^https://[\\S]*/ssl-behind-azure$'), done); 227 | }) 228 | 229 | it('should redirect if activated but wrong flag set (' + method.toUpperCase() + ')', function (done) { 230 | agent 231 | [method]('/ssl-behind-azure') 232 | .set('x-forwarded-proto', 'https') 233 | .expect(301) 234 | .expect('location', new RegExp('^https://[\\S]*/ssl-behind-azure$'), done); 235 | }) 236 | } 237 | 238 | // Test GET requests 239 | proxyTests('get'); 240 | 241 | // Test HEAD requests 242 | proxyTests('head'); 243 | }) 244 | 245 | describe('X-Forwarded-Host redirects', function() { 246 | 247 | var proxyTests = function(method) { 248 | 249 | var app = express(); 250 | 251 | var xArrSslContent = '2048|128|DC=com, DC=microsoft, DC=corp, DC=redmond, CN=MSIT Machine Auth CA 2|C=US, S=WA, L=Redmond, O=Microsoft, OU=OrganizationName, CN=*.azurewebsites.net'; 252 | 253 | app[method]('/ssl', enforce.HTTPS(), 254 | function(req, res){ 255 | res.status(200).send(); 256 | }); 257 | 258 | app[method]('/ssl-with-redirect-trusted', enforce.HTTPS({trustXForwardedHostHeader: true}), 259 | function(req, res){ 260 | res.status(200).send(); 261 | }); 262 | 263 | var agent = request.agent(app); 264 | 265 | it('should ignore x-forwarded-host if not activated (' + method.toUpperCase() + ')', function (done) { 266 | agent 267 | [method]('/ssl') 268 | .set('x-forwarded-host', 'malicious') 269 | .expect(301) 270 | .expect(function(res) { 271 | if(res.header.location.indexOf('malicious') != -1) 272 | throw new Error('should not redirect') 273 | }) 274 | .expect('location', new RegExp('^https://[\\S]*/ssl$'), done); 275 | }) 276 | 277 | it('should ignore x-forwarded-host if not set (' + method.toUpperCase() + ')', function (done) { 278 | agent 279 | [method]('/ssl-with-redirect-trusted') 280 | .expect(301) 281 | .expect(function(res) { 282 | if(res.header.location.indexOf('localhost') != -1 && res.header.location.indexOf('127.0.0.1') != -1) 283 | throw new Error('should not redirect') 284 | }) 285 | .expect('location', new RegExp('^https://[\\S]*/ssl-with-redirect-trusted$'), done); 286 | }) 287 | 288 | it('should redirect if x-forwarded-host and flag is set (' + method.toUpperCase() + ')', function (done) { 289 | agent 290 | [method]('/ssl-with-redirect-trusted') 291 | .set('x-forwarded-host', 'newhost123') 292 | .expect(301) 293 | .expect(function(res) { 294 | if(res.header.location.indexOf('newhost123') === -1) 295 | throw new Error('should redirect') 296 | }) 297 | .expect('location', new RegExp('^https://[\\S]*/ssl-with-redirect-trusted$'), done); 298 | }) 299 | } 300 | 301 | // Test GET requests 302 | proxyTests('get'); 303 | 304 | // Test HEAD requests 305 | proxyTests('head'); 306 | }) 307 | 308 | describe('Pre-1.0.0-style arguments', function() { 309 | 310 | var app = express(); 311 | 312 | app.get('/ssl', enforce.HTTPS(true), 313 | function(req, res){ 314 | res.status(200).send('ok'); 315 | }); 316 | 317 | var agent = request.agent(app); 318 | 319 | it('should crash', function (done) { 320 | agent 321 | .get('/ssl') 322 | .set('x-forwarded-proto', 'https') 323 | .expect(500, done); 324 | }) 325 | }) 326 | }) 327 | --------------------------------------------------------------------------------