Skip to content

Commit

Permalink
nginx: reject HTTPS requests with unexpected Host header
Browse files Browse the repository at this point in the history
  • Loading branch information
alxndrsn committed Oct 9, 2024
1 parent 5c4d261 commit 913ebcc
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 4 deletions.
4 changes: 4 additions & 0 deletions files/nginx/odk.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ server {
listen 443 ssl;
server_name ${CNAME};

if ($http_host != ${CNAME}) {
return 421;
}

ssl_certificate /etc/${SSL_TYPE}/live/${CNAME}/fullchain.pem;
ssl_certificate_key /etc/${SSL_TYPE}/live/${CNAME}/privkey.pem;
ssl_trusted_certificate /etc/${SSL_TYPE}/live/${CNAME}/fullchain.pem;
Expand Down
72 changes: 68 additions & 4 deletions test/test-nginx.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { Readable } = require('stream');
const { assert } = require('chai');

describe('nginx config', () => {
Expand All @@ -12,7 +13,7 @@ describe('nginx config', () => {

// then
assert.equal(res.status, 301);
assert.equal(res.headers.get('location'), 'https://localhost:9000/.well-known/acme-challenge');
assert.equal(res.headers.get('location'), 'https://odk-nginx.example.test/.well-known/acme-challenge');
});

it('well-known should serve from HTTPS', async () => {
Expand All @@ -29,7 +30,7 @@ describe('nginx config', () => {

// then
assert.equal(res.status, 301);
assert.equal(res.headers.get('location'), 'https://localhost:9000/');
assert.equal(res.headers.get('location'), 'https://odk-nginx.example.test/');
});

it('should serve generated client-config.json', async () => {
Expand Down Expand Up @@ -124,16 +125,24 @@ describe('nginx config', () => {
// then
assert.equal(body['x-forwarded-proto'], 'https');
});

it('should reject HTTPS requests with incorrect host header supplied', async () => {
// when
const res = await fetchHttps('/.well-known/acme-challenge', { headers:{ host:'bad.example.com' } });

// then
assert.equal(res.status, 421);
});
});

function fetchHttp(path, options) {
if(!path.startsWith('/')) throw new Error('Invalid path.');
return fetch(`http://localhost:9000${path}`, { redirect:'manual', ...options });
return fetch(`http://localhost:9000${path}`, options);
}

function fetchHttps(path, options) {
if(!path.startsWith('/')) throw new Error('Invalid path.');
return fetch(`https://localhost:9001${path}`, { redirect:'manual', ...options });
return fetch(`https://localhost:9001${path}`, options);
}

function assertEnketoReceived(...expectedRequests) {
Expand Down Expand Up @@ -162,3 +171,58 @@ async function resetMock(port) {
const res = await fetch(`http://localhost:${port}/reset`);
assert.isTrue(res.ok);
}

// Similar to fetch() but:
//
// 1. do not follow redirects
// 2. allow overriding of fetch's "forbidden" headers: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name
function fetch(url, { body, ...options }={}) {
if(!options.headers) options.headers = {};
if(!options.headers.host) options.headers.host = 'odk-nginx.example.test';

return new Promise((resolve, reject) => {
try {
const req = getProtocolImplFrom(url).request(url, options, res => {
res.on('error', reject);

const body = new Readable({ _read: () => {} });
res.on('error', err => body.destroy(err));
res.on('data', data => body.push(data));
res.on('end', () => body.push(null));

const text = () => new Promise((resolve, reject) => {
const chunks = [];
body.on('error', reject);
body.on('data', data => chunks.push(data))
body.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});

const status = res.statusCode;

resolve({
status,
ok: status >= 200 && status < 300,
statusText: res.statusText,
body,
text,
json: async () => JSON.parse(await text()),
headers: new Headers(res.headers),
});
});
req.on('error', reject);
if(body !== undefined) req.write(body);
req.end();
} catch(err) {
reject(err);
}
});
}

function getProtocolImplFrom(url) {
const { protocol } = new URL(url);
switch(protocol) {
case 'http:': return require('node:http');
case 'https:': return require('node:https');
default: throw new Error(`Unsupported protocol: ${protocol}`);
}
}

0 comments on commit 913ebcc

Please sign in to comment.