134 lines
3.8 KiB
JavaScript
134 lines
3.8 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
var SanitizeHost = module.exports;
|
||
|
var HttpMiddleware = require("./http-middleware.js");
|
||
|
|
||
|
SanitizeHost.create = function(gl, app) {
|
||
|
return function(req, res, next) {
|
||
|
function realNext() {
|
||
|
if ("function" === typeof app) {
|
||
|
app(req, res);
|
||
|
} else if ("function" === typeof next) {
|
||
|
next();
|
||
|
} else {
|
||
|
res.statusCode = 500;
|
||
|
res.end("Error: no middleware assigned");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var hostname = HttpMiddleware.getHostname(req);
|
||
|
// Replace the hostname, and get the safe version
|
||
|
var safehost = HttpMiddleware.sanitizeHostname(req);
|
||
|
|
||
|
// if no hostname, move along
|
||
|
if (!hostname) {
|
||
|
realNext();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// if there were unallowed characters, complain
|
||
|
if (safehost.length !== hostname.length) {
|
||
|
res.statusCode = 400;
|
||
|
res.end("Malformed HTTP Header: 'Host: " + hostname + "'");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Note: This sanitize function is also called on plain sockets, which don't need Domain Fronting checks
|
||
|
if (req.socket.encrypted) {
|
||
|
if (req.socket && "string" === typeof req.socket.servername) {
|
||
|
// Workaround for https://github.com/nodejs/node/issues/22389
|
||
|
if (!SanitizeHost._checkServername(safehost, req.socket)) {
|
||
|
res.statusCode = 400;
|
||
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||
|
res.end(
|
||
|
"<h1>Domain Fronting Error</h1>" +
|
||
|
"<p>This connection was secured using TLS/SSL for '" +
|
||
|
(req.socket.servername || "").toLowerCase() +
|
||
|
"'</p>" +
|
||
|
"<p>The HTTP request specified 'Host: " +
|
||
|
safehost +
|
||
|
"', which is (obviously) different.</p>" +
|
||
|
"<p>Because this looks like a domain fronting attack, the connection has been terminated.</p>"
|
||
|
);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
/*
|
||
|
else if (safehost && !gl._skip_fronting_check) {
|
||
|
|
||
|
// We used to print a log message here, but it turns out that it's
|
||
|
// really common for IoT devices to not use SNI (as well as many bots
|
||
|
// and such).
|
||
|
// It was common for the log message to pop up as the first request
|
||
|
// to the server, and that was confusing. So instead now we do nothing.
|
||
|
|
||
|
//console.warn("no string for req.socket.servername," + " skipping fronting check for '" + safehost + "'");
|
||
|
//gl._skip_fronting_check = true;
|
||
|
}
|
||
|
*/
|
||
|
}
|
||
|
|
||
|
// carry on
|
||
|
realNext();
|
||
|
};
|
||
|
};
|
||
|
|
||
|
var warnDomainFronting = true;
|
||
|
var warnUnexpectedError = true;
|
||
|
SanitizeHost._checkServername = function(safeHost, tlsSocket) {
|
||
|
var servername = (tlsSocket.servername || "").toLowerCase();
|
||
|
|
||
|
// acceptable: older IoT devices may lack SNI support
|
||
|
if (!servername) {
|
||
|
return true;
|
||
|
}
|
||
|
// acceptable: odd... but acceptable
|
||
|
if (!safeHost) {
|
||
|
return true;
|
||
|
}
|
||
|
if (safeHost === servername) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if ("function" !== typeof tlsSocket.getCertificate) {
|
||
|
// domain fronting attacks allowed
|
||
|
if (warnDomainFronting) {
|
||
|
// https://github.com/nodejs/node/issues/24095
|
||
|
console.warn(
|
||
|
"Warning: node " +
|
||
|
process.version +
|
||
|
" is vulnerable to domain fronting attacks. Please use node v11.2.0 or greater."
|
||
|
);
|
||
|
warnDomainFronting = false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// connection established with servername and session is re-used for allowed name
|
||
|
// See https://github.com/nodejs/node/issues/24095
|
||
|
var cert = tlsSocket.getCertificate();
|
||
|
try {
|
||
|
// TODO optimize / cache?
|
||
|
// *should* always have a string, right?
|
||
|
// *should* always be lowercase already, right?
|
||
|
if (
|
||
|
(cert.subject.CN || "").toLowerCase() !== safeHost &&
|
||
|
!(cert.subjectaltname || "").split(/,\s+/).some(function(name) {
|
||
|
// always prefixed with "DNS:"
|
||
|
return safeHost === name.slice(4).toLowerCase();
|
||
|
})
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
// not sure what else to do in this situation...
|
||
|
if (warnUnexpectedError) {
|
||
|
console.warn("Warning: encoutered error while performing domain fronting check: " + e.message);
|
||
|
warnUnexpectedError = false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
};
|