← Blog

Claude skill TLS certificate validation security

MCP skills that make outbound HTTPS requests to APIs, webhooks, or data sources depend on TLS certificate validation to confirm that the remote server is who it claims to be. When certificate validation is bypassed — through a one-line option, an override function, an environment variable, or a deprecated protocol version — every outbound connection becomes exploitable by a network-positioned attacker performing man-in-the-middle interception. The attacker reads and modifies API responses in transit, injects malicious tool call responses, and captures credentials and tokens exchanged with third-party services. SA-SKL-TLS covers four TLS certificate validation bypass patterns that are common in MCP skill codebases: rejectUnauthorized: false in request options, a checkServerIdentity override that always returns undefined, outdated minVersion TLS protocol settings, and the process-wide NODE_TLS_REJECT_UNAUTHORIZED=0 environment variable left over from debugging.

By SkillAudit  |  3 Nov 2026

TLS validation bypasses are among the most dangerous single-line vulnerabilities in backend code because their effect is total and invisible — every outbound HTTPS call silently becomes an unencrypted, unverified channel once the bypass is in place. In MCP skills the blast radius is especially high: a skill typically holds API keys, OAuth tokens, and database credentials that it forwards in outbound request headers. An attacker on the same network as the skill host (a shared cloud VPC, a container network, a compromised dependency running a local proxy) intercepts all of this. The fix in every case is a one-line removal, but the bypass patterns are easy to introduce during local development with self-signed certificates and hard to find in code review because they are buried in HTTP client options objects.

SA-SKL-TLS-001: rejectUnauthorized: false in HTTPS request options — certificate validation disabled

The most common TLS bypass in Node.js skills is passing { rejectUnauthorized: false } in the options object to https.request(), https.get(), or the agent option of a higher-level HTTP library. The option disables all certificate chain validation for that connection: expired certificates, self-signed certificates, certificates for the wrong hostname, and certificates signed by an untrusted CA are all accepted silently. This is the standard workaround for local development with a self-signed certificate, but it routinely makes it into production because there is no runtime warning when the option is set:

// VULNERABLE: rejectUnauthorized:false disables all certificate validation
import https from 'https';

async function callExternalApi(endpoint: string, token: string): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const req = https.request(endpoint, {
      method: 'GET',
      headers: { Authorization: `Bearer ${token}` },
      rejectUnauthorized: false,  // <-- bypass: certificate not validated
    }, (res) => {
      let body = '';
      res.on('data', (chunk) => { body += chunk; });
      res.on('end', () => resolve(JSON.parse(body)));
    });
    req.on('error', reject);
    req.end();
  });
}

// ALSO VULNERABLE: same option in axios, node-fetch, got, undici, etc.
import axios from 'axios';
const client = axios.create({
  httpsAgent: new https.Agent({ rejectUnauthorized: false }), // bypass
});

// ATTACK: network-positioned MITM
// 1. Attacker positions between skill host and target API server
//    (shared container network, VPC peering, ARP spoofing, DNS hijack)
// 2. Attacker presents any certificate (self-signed, expired, wrong CN)
// 3. Node.js accepts it because rejectUnauthorized:false skips chain validation
// 4. TLS handshake completes normally to attacker's proxy
// 5. Attacker reads: Authorization header with Bearer token
//    Attacker modifies: response body → inject malicious tool data, false AI responses
// 6. All subsequent skill calls to the same endpoint are compromised
//    Token extracted in step 5 can be replayed from outside the skill's network

// SECURE: remove rejectUnauthorized:false and handle the root cause instead
// Root cause 1 (self-signed cert): add CA to NODE_EXTRA_CA_CERTS
// Root cause 2 (internal PKI): bundle the CA cert with the skill container
// Root cause 3 (expired cert): rotate the certificate
const secureReq = https.request(endpoint, {
  method: 'GET',
  headers: { Authorization: `Bearer ${token}` },
  // rejectUnauthorized defaults to true — do not override it
});

// If you need to trust an internal CA (e.g. a corporate PKI):
import { readFileSync } from 'fs';
const secureClient = axios.create({
  httpsAgent: new https.Agent({
    ca: readFileSync('/etc/ssl/internal-ca.pem'),  // trust your CA
    // rejectUnauthorized is implicitly true — certificate IS checked against this CA
  }),
});

Development self-signed certificate is not a valid reason to ship rejectUnauthorized: false: The correct fix is to add your internal CA certificate to NODE_EXTRA_CA_CERTS (a Node.js environment variable that appends a PEM file to the built-in CA store) or to pass the CA cert explicitly in the ca option. rejectUnauthorized: false does not just trust self-signed certs — it trusts all certificates, including those presented by an attacker. There is never a production use case that justifies disabling certificate validation entirely.

CRITICAL
SA-SKL-TLS-001 — rejectUnauthorized: false in HTTPS request options: certificate chain and hostname validation completely disabled for the connection; network-positioned attacker presents any certificate and intercepts all request/response data including Authorization headers, API keys, and response bodies. Fix: remove the option; handle the root cause (self-signed CA) via NODE_EXTRA_CA_CERTS or the ca option. Detection: literal rejectUnauthorized: false or rejectUnauthorized:false in source → CRITICAL.

SA-SKL-TLS-002: checkServerIdentity override that always returns undefined — hostname validation disabled

Node.js allows overriding the server identity check that runs after the certificate chain is validated. The checkServerIdentity function receives the hostname and the parsed certificate object; if it returns undefined, the connection proceeds; if it throws an error, the connection is rejected. Developers override this function to suppress hostname mismatch errors when working with internal services that use wildcard certificates or certificates with the wrong Common Name. The override that always returns undefined (or returns without throwing) silently accepts any certificate for any hostname, defeating the hostname binding that is the second half of TLS authentication:

// VULNERABLE: checkServerIdentity override suppresses all hostname validation errors
import https from 'https';
import tls from 'tls';

const insecureAgent = new https.Agent({
  checkServerIdentity: (hostname, cert) => {
    // "fix" for SAN mismatch — returns undefined unconditionally
    return undefined;
  },
});

// Also common: empty function body
const alsoInsecure = new https.Agent({
  checkServerIdentity: () => {},  // returns undefined — hostname check suppressed
});

// And the no-op arrow
const alsoInsecure2 = new https.Agent({
  checkServerIdentity: (host, cert) => undefined,
});

// ATTACK: hostname mismatch exploitation
// Scenario: skill connects to https://api.billing.internal (internal service)
// Attacker controls DNS for api.billing.internal (internal DNS compromise, or
//   DNS spoofing on the container network)
// Attacker redirects api.billing.internal → attacker's server
// Attacker presents a valid certificate for a DIFFERENT hostname:
//   e.g. a Let's Encrypt cert for attacker-controlled.example.com
// Without checkServerIdentity override: Node.js rejects — CN/SAN mismatch
// With checkServerIdentity: () => undefined: Node.js accepts — MITM complete
// Attacker now intercepts all billing API calls, reads invoice data, issues credits

// ALSO VULNERABLE: checkServerIdentity that only checks cert.subject.CN
// but not cert.subjectaltname (SANs take precedence over CN in TLS 1.3+)

// SECURE: use the built-in check as a base, only relax specific constraints
const secureAgent = new https.Agent({
  checkServerIdentity: (hostname, cert) => {
    // Run the built-in check first — throws on mismatch
    const err = tls.checkServerIdentity(hostname, cert);
    if (err) {
      // Only suppress for explicitly known internal hosts, never wildcard suppress
      const allowedInternalHosts = new Set(['api.billing.internal', 'db.internal']);
      if (allowedInternalHosts.has(hostname)) {
        // Log the suppression so it's visible in audit logs
        console.warn(`TLS hostname check suppressed for known internal host: ${hostname}`);
        return undefined;  // explicitly allowed
      }
      throw err;  // all other mismatches are fatal
    }
    return undefined;  // built-in check passed
  },
});

// BEST PRACTICE: fix the certificate instead of suppressing the check
// Add the correct SAN/CN to your internal certificate via your PKI
// Use cert-manager (Kubernetes) or AWS ACM Private CA for internal certs

The difference between rejectUnauthorized: false and an always-returning checkServerIdentity: rejectUnauthorized: false skips the entire certificate chain validation — expired certs, self-signed certs, and untrusted CAs are all accepted. An always-returning checkServerIdentity runs the chain validation but skips the hostname binding step. Both result in a fully exploitable MITM surface; the difference is only in which attack vector they enable. A network attacker who controls DNS needs only the second bypass; an attacker who can present any certificate needs the first.

CRITICAL
SA-SKL-TLS-002 — checkServerIdentity override always returns undefined: hostname binding check disabled; attacker with DNS control or network routing control presents a certificate for a different hostname; Node.js accepts the connection; MITM complete. Fix: call tls.checkServerIdentity(hostname, cert) as the base check; only suppress for an explicit allowlist of known internal hostnames; log every suppression. Detection: checkServerIdentity: () => returning without calling base check → CRITICAL.

SA-SKL-TLS-003: Deprecated TLS version allowed via minVersion option — BEAST and POODLE exposure

Node.js allows configuring the minimum TLS protocol version accepted for a connection. TLSv1.0 and TLSv1.1 were deprecated by RFC 8996 in March 2021 and are disabled by default in modern OpenSSL builds and Node.js ≥18. However, MCP skill codebases sometimes explicitly re-enable them via the minVersion option when connecting to legacy internal services, or inherit them from an older dependency’s HTTPS agent configuration. Explicitly setting minVersion: 'TLSv1' or 'TLSv1.1' overrides the Node.js default and re-exposes the connection to attacks against these protocols:

// VULNERABLE: explicitly re-enabling deprecated TLS versions
import https from 'https';

const legacyCompatAgent = new https.Agent({
  minVersion: 'TLSv1',    // TLSv1.0: BEAST (CBC chosen-plaintext), POODLE-TLS
  // or:
  minVersion: 'TLSv1.1',  // TLSv1.1: still vulnerable to some CBC padding attacks
});

// Also vulnerable: secureProtocol option (older API, still works)
const alsoVulnerable = new https.Agent({
  secureProtocol: 'TLSv1_method',  // forces TLS 1.0 specifically
});

// ATTACK SURFACE: BEAST (Browser Exploit Against SSL/TLS)
// TLS 1.0 uses CBC mode with predictable IV chaining
// Chosen-plaintext attack recovers plaintext of HTTPS cookies
// In MCP skill context: session tokens, API keys in Cookie header
// Practical exploit: attacker with network visibility + BEAST injection
//   can recover session token from TLS 1.0 traffic in <1000 requests

// ATTACK SURFACE: POODLE (Padding Oracle On Downgraded Legacy Encryption)
// SSL 3.0 / TLS 1.0: CBC padding not fully authenticated
// POODLE variant against TLS: some TLS 1.0 implementations vulnerable
// Practical: session hijacking via padding oracle in MitM position

// NODE.JS DEFAULT: TLSv1.2 minimum since Node 18 (OpenSSL 3.0)
// Explicitly overriding minVersion downgrades below the safe default

// SECURE: do not set minVersion below TLSv1.2
import https from 'https';
import tls from 'tls';

const secureAgent = new https.Agent({
  minVersion: 'TLSv1.2',  // explicit, not relying on the default
  // For new services that you control: TLSv1.3 is preferred
  // minVersion: 'TLSv1.3',  // only TLS 1.3 — eliminates all CBC-mode cipher suites
});

// STARTUP VALIDATION: detect accidental downgrade early
function validateTlsConfig() {
  const allowedVersions = new Set(['TLSv1.2', 'TLSv1.3']);
  const minVer = process.env.NODE_OPTIONS?.match(/--tls-min-v(\S+)/)?.[1];
  if (minVer && !allowedVersions.has(`TLSv${minVer}`)) {
    throw new Error(`Insecure TLS minVersion configured via NODE_OPTIONS: TLSv${minVer}`);
  }
}

// If you must connect to a legacy service that requires TLS 1.0/1.1:
// Use a dedicated sidecar proxy (Envoy, nginx) for the legacy connection
// and connect to the proxy on localhost/TLS 1.3
// Never lower the minimum version for the entire process

Legacy internal services are not a valid justification for minVersion: 'TLSv1': If you must communicate with a legacy service that cannot be upgraded to TLS 1.2, use a dedicated reverse proxy sidecar (nginx ssl_protocols TLSv1 TLSv1.1 on the proxy facing the legacy service, TLS 1.3 on the proxy facing your skill). The TLS downgrade is then isolated to a single controlled hop rather than applied process-wide to all connections made by the skill. The sidecar pattern is the correct architectural solution for legacy TLS requirements.

HIGH
SA-SKL-TLS-003 — Deprecated TLS version via minVersion: 'TLSv1' or 'TLSv1.1': CBC-mode cipher suites enabled; BEAST chosen-plaintext and POODLE padding oracle attacks applicable; session tokens and API keys in transit at risk. Fix: set minVersion: 'TLSv1.2' minimum; use 'TLSv1.3' for new services; isolate legacy TLS requirements behind a sidecar proxy. Detection: minVersion: 'TLSv1', 'TLSv1.1', or secureProtocol: 'TLSv1_method' in source → HIGH.

SA-SKL-TLS-004: NODE_TLS_REJECT_UNAUTHORIZED=0 environment variable — process-wide validation disabled

The NODE_TLS_REJECT_UNAUTHORIZED environment variable is a process-wide override that disables TLS certificate validation for every HTTPS connection made by the Node.js process. Setting it to 0 is equivalent to passing rejectUnauthorized: false in every HTTPS request options object simultaneously. It is used during local development to bypass TLS issues without modifying source code, and it is the standard suggestion in many stack overflow answers and framework documentation pages. The critical failure mode is that this variable is set in the skill’s deployment environment (a Dockerfile, a container environment spec, a cloud run service YAML, a CI secret) and never removed after the original debugging session:

// VULNERABLE: environment variable that disables TLS validation process-wide
// Set in one of these common locations:

// 1. Dockerfile (most dangerous — permanently in the container image)
ENV NODE_TLS_REJECT_UNAUTHORIZED=0

// 2. docker-compose.yml
environment:
  - NODE_TLS_REJECT_UNAUTHORIZED=0

// 3. .env file (picked up by dotenv / Docker Compose)
NODE_TLS_REJECT_UNAUTHORIZED=0

// 4. Shell startup script / entrypoint.sh
export NODE_TLS_REJECT_UNAUTHORIZED=0
node dist/index.js

// 5. CI/CD pipeline secrets (most subtle — appears at runtime, not in code)
// GitHub Actions: ACTIONS_RUNNER_DEBUG=true often paired with TLS bypass

// EFFECT: every HTTPS call made by the skill, by any dependency,
//   by any SDK, is now unvalidated
// Including: Anthropic SDK calls, database TLS connections,
//   monitoring agent HTTPS, logging SDK, OpenTelemetry exporter

// ATTACK: container network MITM
// Skill deployed in Kubernetes or Docker Compose with NODE_TLS_REJECT_UNAUTHORIZED=0
// Compromised sidecar container or network plugin intercepts traffic
// Attacker reads all outbound API calls: Anthropic API key in Authorization header,
//   database connection string parameters, OAuth client secrets
// No code change needed — the env var does the work

// SECURE: startup validation that throws if the env var is set to 0
// Add this to your skill's initialization code, before any HTTPS calls

function assertTlsValidationEnabled(): void {
  if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
    throw new Error(
      'NODE_TLS_REJECT_UNAUTHORIZED=0 is set — TLS certificate validation is disabled. ' +
      'This is not allowed in production. Remove the variable or set it to 1. ' +
      'If you need to trust a custom CA, use NODE_EXTRA_CA_CERTS=/path/to/ca.pem instead.'
    );
  }
  // Also check for the deprecated equivalent
  if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === 'false') {
    throw new Error('NODE_TLS_REJECT_UNAUTHORIZED=false is equivalent to =0 — same fix applies.');
  }
}

// Call at the top of your skill's entry point, before any SDK initialization
assertTlsValidationEnabled();

// ALTERNATIVE: if the cert is from an internal CA, use this instead:
// NODE_EXTRA_CA_CERTS=/etc/ssl/my-internal-ca.pem
// This appends your CA to the built-in CA store WITHOUT disabling validation
// All certificates signed by your CA are trusted; all others still require a valid chain

NODE_TLS_REJECT_UNAUTHORIZED=0 affects every library in the process, not just your code: Setting this variable disables TLS validation for the Anthropic SDK’s HTTPS calls, for any database driver that uses TLS, for any OpenTelemetry or logging SDK exporter, and for any other HTTP client library in the entire Node.js process. A single environment variable in a Dockerfile exposes your Anthropic API key, database credentials, and all third-party service tokens to any network-positioned attacker. The startup check that throws on this variable is the most high-leverage TLS security control you can add to a skill in a single line.

CRITICAL
SA-SKL-TLS-004 — NODE_TLS_REJECT_UNAUTHORIZED=0 in Dockerfile, .env, or deployment config: process-wide TLS validation disabled for all HTTP libraries including Anthropic SDK; network-positioned attacker intercepts all outbound connections. Fix: startup assertTlsValidationEnabled() check that throws if the variable is set; use NODE_EXTRA_CA_CERTS for internal CA trust instead. Detection: literal NODE_TLS_REJECT_UNAUTHORIZED=0 in Dockerfile, .env, YAML, or shell scripts → CRITICAL.

Remediation summary

FindingSeverityFix
TLS-001: rejectUnauthorized: false in request optionsCRITICALRemove the option; fix root cause via NODE_EXTRA_CA_CERTS or the ca option for internal CAs; never disable validation entirely
TLS-002: checkServerIdentity always returns undefinedCRITICALCall tls.checkServerIdentity(hostname, cert) as the base; suppress only for an explicit hostname allowlist; log every suppression
TLS-003: minVersion: 'TLSv1' or 'TLSv1.1'HIGHSet minVersion: 'TLSv1.2' minimum; use sidecar proxy for legacy service requirements; prefer 'TLSv1.3' for new services
TLS-004: NODE_TLS_REJECT_UNAUTHORIZED=0 in environmentCRITICALStartup check throws if variable is set; use NODE_EXTRA_CA_CERTS for internal CA trust; scan Dockerfiles and env specs for the variable

SkillAudit detects all four SA-SKL-TLS-001 through TLS-004 patterns through static analysis of skill source code and configuration files: rejectUnauthorized: false in HTTPS options, checkServerIdentity overrides that do not call the base check, minVersion values below TLSv1.2, and NODE_TLS_REJECT_UNAUTHORIZED=0 in Dockerfiles, .env files, and shell scripts included in the repository. For related security patterns see SA-SKL-NET network egress security, SA-SKL-SSRF server-side request forgery, and SA-SKL-KMG key management security. See the complete SA-SKL attack surface reference for the full vulnerability taxonomy.

Scan your Claude skill for TLS certificate validation vulnerabilities

SkillAudit detects rejectUnauthorized bypasses, broken checkServerIdentity overrides, deprecated TLS versions, and NODE_TLS_REJECT_UNAUTHORIZED misconfigurations across the full SA-SKL-TLS family in 60 seconds.

Get your free audit