Description
Disabling server certificate validation, for example with rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0, prevents a TLS client from reliably authenticating the server. An attacker on the network path may present an untrusted certificate and impersonate the intended server. Interception through a hostile proxy, gateway, or public network may expose sensitive data or allow response manipulation and session theft.
Potential impact
- A man-in-the-middle attacker may establish a connection using a fake certificate.
- Credentials, tokens, or personal information may be exposed to an impersonating server. TLS encryption itself is not removed.
- The attacker may modify responses or insert malicious payloads.
- Users or applications may communicate with an attacker while believing it is the legitimate service.
Remediation
- Remove
rejectUnauthorizedor retain its defaulttruevalue. - Do not set
process.env.NODE_TLS_REJECT_UNAUTHORIZED=0to disable validation globally. - Supply trusted private or public CA certificates through
cainhttps.Agentortls.connect. - Keep hostname verification enabled.
tls.connectdoes not enable SNI automatically; setservernamealongsidehostwhen the server requires it. - If pinning is required, compare with a trusted certificate or public key and plan for certificate changes. The
fingerprint256example below compares the entire certificate's SHA-256 fingerprint, not its public key. - Isolate any temporary test-only exception from production configuration and builds.
Examples
Before
javascript
// Before, example 1: disable certificate validation globally
const https = require('https');
const tls = require('tls');
function fetchInsecure() {
// Disabling TLS validation globally is dangerous
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
https.get({ hostname: 'api.example.org', path: '/v1/data' }, (res) => {
res.on('data', () => {});
});
}
// Before, example 2: disable certificate validation in an agent
function postInsecure() {
const insecureAgent = new https.Agent({ keepAlive: true, rejectUnauthorized: false });
const req = https.request({
hostname: 'secure.example.org',
method: 'POST',
path: '/login',
agent: insecureAgent,
headers: { 'Content-Type': 'application/json' }
}, (res) => res.resume());
req.write(JSON.stringify({ user: 'alice', pass: 'secret' }));
req.end();
}
After
javascript
// After: retain default validation and use a trusted CA bundle
const https = require('https');
const fs = require('fs');
// Example path to a private or public CA bundle
const caBundle = fs.readFileSync('/etc/ssl/certs/company-ca-bundle.pem');
function fetchSecure() {
// rejectUnauthorized defaults to true
const secureAgent = new https.Agent({
keepAlive: true,
ca: caBundle, // Supply trusted CAs
// https derives servername from the request host; do not disable it
});
https.get({
hostname: 'api.example.org',
path: '/v1/data',
agent: secureAgent
}, (res) => {
let body = '';
res.on('data', (c) => body += c);
res.on('end', () => console.log('OK:', body.length));
});
}
// Optional: certificate pinning using the certificate SHA-256 fingerprint
const tls = require('tls');
function connectWithPinning(expectedFingerprint256) {
const socket = tls.connect({ host: 'secure.example.org', port: 443, ca: caBundle }, () => {
const cert = socket.getPeerCertificate(true);
if (!cert || !cert.fingerprint256 || cert.fingerprint256 !== expectedFingerprint256) {
socket.destroy(new Error('Certificate pinning mismatch'));
return;
}
socket.write('GET / HTTP/1.1\r\nHost: secure.example.org\r\n\r\n');
});
}
Explanation:
- Before:
rejectUnauthorized: falseorNODE_TLS_REJECT_UNAUTHORIZED=0bypasses certificate and hostname validation failures, allowing an attacker with a fake certificate to impersonate the server. - After: Default validation (
rejectUnauthorized: true) and a trusted CA bundle authenticate the server. Optional pinning adds another identity check. Also verify CA trust, hostname validation, and behavior during certificate changes in production.