Feat: Add warning for cert. hostname mismatch (#3942)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Nelson Chan
2025-12-01 10:12:47 +08:00
committed by GitHub
parent b230ab0a06
commit 5bf9a51522
6 changed files with 89 additions and 10 deletions

View File

@@ -9,7 +9,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../../src/util");
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius,
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, checkCertificateHostname
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -565,6 +565,7 @@ class Monitor extends BeanModel {
tlsSocket.once("secureConnect", async () => {
tlsInfo = checkCertificate(tlsSocket);
tlsInfo.valid = tlsSocket.authorized || false;
tlsInfo.hostnameMatchMonitorUrl = checkCertificateHostname(tlsInfo.certInfo.raw, this.getUrl()?.hostname);
await this.handleTlsInfo(tlsInfo);
});
@@ -587,6 +588,7 @@ class Monitor extends BeanModel {
if (tlsSocket) {
tlsInfo = checkCertificate(tlsSocket);
tlsInfo.valid = tlsSocket.authorized || false;
tlsInfo.hostnameMatchMonitorUrl = checkCertificateHostname(tlsInfo.certInfo.raw, this.getUrl()?.hostname);
await this.handleTlsInfo(tlsInfo);
}

View File

@@ -636,6 +636,30 @@ exports.checkCertificate = function (socket) {
};
};
/**
* Checks if the certificate is valid for the provided hostname.
* Defaults to true if feature `X509Certificate` is not available, or input is not valid.
* @param {Buffer} certBuffer - The certificate buffer.
* @param {string} hostname - The hostname to compare against.
* @returns {boolean} True if the certificate is valid for the provided hostname, false otherwise.
*/
exports.checkCertificateHostname = function (certBuffer, hostname) {
let X509Certificate;
try {
X509Certificate = require("node:crypto").X509Certificate;
} catch (_) {
// X509Certificate is not available in this version of Node.js
return true;
}
if (!X509Certificate || !certBuffer || !hostname) {
return true;
}
let certObject = new X509Certificate(certBuffer);
return certObject.checkHost(hostname) !== undefined;
};
/**
* Check if the provided status code is within the accepted ranges
* @param {number} status The status code to check

View File

@@ -12,6 +12,7 @@ import {
faArrowUp,
faCog,
faEdit,
faExclamationTriangle,
faEye,
faEyeSlash,
faList,
@@ -60,6 +61,7 @@ library.add(
faArrowUp,
faCog,
faEdit,
faExclamationTriangle,
faEye,
faEyeSlash,
faList,

View File

@@ -445,6 +445,7 @@
"Query": "Query",
"settingsCertificateExpiry": "TLS Certificate Expiry",
"certificationExpiryDescription": "HTTPS Monitors trigger notification when TLS certificate expires in:",
"certHostnameMismatch": "Certificate hostname does not match the monitor URL.",
"Setup Docker Host": "Set Up Docker Host",
"Connection Type": "Connection Type",
"Docker Daemon": "Docker Daemon",

View File

@@ -294,15 +294,8 @@
/>)
</p>
<span class="col-4 col-sm-12 num">
<a
href="#"
@click.prevent="
toggleCertInfoBox = !toggleCertInfoBox
"
>{{ tlsInfo.certInfo.daysRemaining }}
{{
$tc("day", tlsInfo.certInfo.daysRemaining)
}}</a>
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
<font-awesome-icon v-if="tlsInfo.hostnameMatchMonitorUrl === false" class="cert-info-warn" icon="exclamation-triangle" :title="$t('certHostnameMismatch')" />
</span>
</div>
</div>
@@ -1140,4 +1133,14 @@ table {
opacity: 0.7;
}
}
.cert-info-warn {
margin-left: 4px;
opacity: 0.5;
.dark & {
opacity: 0.7;
}
}
</style>

View File

@@ -0,0 +1,47 @@
const { test } = require("node:test");
const assert = require("node:assert");
const { checkCertificateHostname } = require("../../server/util-server");
const testCert = `
-----BEGIN CERTIFICATE-----
MIIFCTCCA/GgAwIBAgISBEROD0/r+BjpW4TvWCcZYxjpMA0GCSqGSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
EwJSMzAeFw0yMzA5MDQxMjExMThaFw0yMzEyMDMxMjExMTdaMBQxEjAQBgNVBAMM
CSouZWZmLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALywpmHr
GOFlhw9CcW11fVloL6dceeUexbIwVd/gOt0/rIlgBViOGCh1pFYA/Essty4vXBzx
cp6W4WurmwU6ZOJA0/T6rxnmsjxSdrHVGBGgW18HJ9IWqBl9MigjpRo9h4SlAPJq
cAsiBfPhQ0oSe/8IqwgKA4HTvlcTf5/HKnbe0MyQt7WNILWHm+zpfLE0AmLVXxqA
MNc/ynQDLTsWDZnqqri4MKOW1yOAMbUoAWSsNaagoGnZU4bg8uhu/2JTi/vdjl0g
fTDOjsELc70cWekZ9Mv4ND4w3SEthotbMCCtZE5bUqcGzSm4pQEJ37kQ7xjJ0onT
RRcuZI6/jDWzwZ0CAwEAAaOCAjUwggIxMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
hTqVTd8TZ2pknzGJtKw2JaIrPJAwHwYDVR0jBBgwFoAUFC6zF7dYVsuuUAlA5h+v
nYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRwOi8vcjMuby5s
ZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pLmxlbmNyLm9yZy8wPwYD
VR0RBDgwNoIJKi5lZmYub3JnghEqLnN0YWdpbmcuZWZmLm9yZ4IWd3d3Lmh0dHBz
LXJ1bGVzZXRzLm9yZzATBgNVHSAEDDAKMAgGBmeBDAECATCCAQMGCisGAQQB1nkC
BAIEgfQEgfEA7wB2ALc++yTfnE26dfI5xbpY9Gxd/ELPep81xJ4dCYEl7bSZAAAB
imBRp0EAAAQDAEcwRQIhAMW3HZwWZWXPWfahH2pr/lxCcoSluHv2huAW6rlzU3zn
AiAOzD/p8F3gT1bzDgdSW+X5WDBeU+EutRbHMSV+Cx0mZwB1AHoyjFTYty22IOo4
4FIe6YQWcDIThU070ivBOlejUutSAAABimBRqRQAAAQDAEYwRAIgFXvRRZS3xx83
XdTsnto5SxSnGi1+YfzYobMdV1yqHGACIDurLvkt58TwifUbyXflGZJmOMhcC2G1
KUd29yCUjIahMA0GCSqGSIb3DQEBCwUAA4IBAQA6t2F3PKMLlb2A/JsQhPFUJLS3
6cx+97dzROQLBdnUQIMxPkJBN/lltNdsVxJa4A3DMbrJOayefX2l8UIvFiEFVseF
WrxbmXDF68fwhBKBgeqZ25/S8jEdP5PWYWXHgXvx0zRdhfe9vuba5WeFyz79cR7K
t3bSyv6GMJ2z3qBkVFGHSeYakcxPWes3CNmGxInwZNBXA2oc7xuncFrjno/USzUI
nEefDfF3H3jC+0iP3IpsK8orwgWz4lOkcMYdan733lSZuVJ6pm7C9phTV04NGF3H
iPenGDCg1awOyRnvxNq1MtMDkR9AHwksukzwiYNexYjyvE2t0UzXhFXwazQ3
-----END CERTIFICATE-----
`;
test("Certificate and hostname match", () => {
const result = checkCertificateHostname(testCert, "www.eff.org");
assert.strictEqual(result, true);
});
test("Certificate and hostname mismatch", () => {
const result = checkCertificateHostname(testCert, "example.com");
assert.strictEqual(result, false);
});