TLS 인증서 검증 비활성화

Improper Certificate Validation

설명

TLS 연결에서 서버 인증서 검증을 끄면(예: rejectUnauthorized: false 또는 NODE_TLS_REJECT_UNAUTHORIZED=0) 서버의 신원을 확인할 수 없습니다. 이 때문에 공격자가 임의(자체 서명 또는 위조) 인증서를 제시해도 연결이 성립하며, 사용자는 합법 서버라고 착각하게 됩니다. 실제 공격 시 공용 Wi‑Fi, 프록시, 악성 게이트웨이 등을 통해 트래픽을 가로채고 가짜 인증서를 주입하여 민감정보 탈취, 응답 변조, 세션 하이재킹이 가능합니다.

잠재적 영향

  • 중간자(MitM) 공격 노출: 공격자가 통신을 가로채어 가짜 인증서를 제시해도 연결이 성공합니다.
  • 데이터 유출: 로그인 정보, 토큰, 개인정보 등 민감 데이터가 서버를 가장한 공격자에게 노출될 수 있습니다. TLS 암호화 자체가 없어지는 것은 아닙니다.
  • 응답 변조/코드 주입: 전달되는 응답을 변경하거나 악성 페이로드를 삽입할 수 있습니다.
  • 서비스 가장(Spoofing): 공격자가 합법 서버로 위장하여 사용자를 속일 수 있습니다.

해결 방법

  • rejectUnauthorized 옵션을 제거하거나 기본값(true)로 유지하세요.
  • 전역 비활성화 금지: process.env.NODE_TLS_REJECT_UNAUTHORIZED=0 설정을 사용하지 마세요.
  • 신뢰 가능한 CA 제공: 사내 CA 또는 공인 CA 번들을 ca 옵션(https.Agent/tls.connect)에 설정하세요.
  • 서버명 검증 유지: 호스트명 검증을 끄지 마세요. tls.connect는 SNI를 자동으로 켜지 않으므로 서버에 필요하면 host와 함께 servername을 설정하세요.
  • 필요 시 Certificate Pinning 적용: 신뢰한 인증서나 공개키와 일치하는지 추가 검증하고, 인증서 교체 계획도 준비하세요. 아래 fingerprint256 예시는 공개키가 아니라 인증서 전체의 SHA-256 지문을 비교합니다.
  • 테스트 전용 완화: 테스트 환경에서만 임시 우회가 필요하다면, 별도 테스트 설정으로 분리하고 운영 빌드에 포함되지 않게 하세요.

예시

변경 전

javascript
// 비준수 예시 1: 전역 인증서 검증 비활성화
const https = require('https');
const tls = require('tls');

function fetchInsecure() {
  // 전역으로 TLS 검증을 비활성화 -> 매우 위험
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

  https.get({ hostname: 'api.example.org', path: '/v1/data' }, (res) => {
    res.on('data', () => {});
  });
}

// 비준수 예시 2: 에이전트에서 인증서 검증 비활성화
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();
}

변경 후

javascript
// 준수 예시: 기본 검증 유지 + 신뢰 가능한 CA 번들 사용
const https = require('https');
const fs = require('fs');

// 사내 또는 공인 CA 번들 경로(예시)
const caBundle = fs.readFileSync('/etc/ssl/certs/company-ca-bundle.pem');

function fetchSecure() {
  // rejectUnauthorized 기본값은 true
  const secureAgent = new https.Agent({
    keepAlive: true,
    ca: caBundle,           // 신뢰 가능한 CA 제공
    // servername은 요청 host에서 자동 설정되므로 따로 끄지 않음
  });

  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));
  });
}

// 추가: 필요 시 인증서 핀닝(인증서 SHA-256 지문 비교) 예시
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');
  });
}

설명:

  • 변경 전: rejectUnauthorized: false나 NODE_TLS_REJECT_UNAUTHORIZED=0은 인증서와 호스트명 검증을 비활성화하여, 공격자가 임의의(가짜) 인증서를 제시해도 연결이 성공하게 만듭니다. 이로 인해 공용 네트워크나 악성 프록시를 통한 중간자 공격에 매우 취약해집니다.
  • 변경 후: 기본 검증을 유지하고(=rejectUnauthorized: true) 신뢰 가능한 CA 번들을 제공하여 서버 신원을 정상적으로 검증합니다. 필요 시 인증서 핀닝을 추가해 위조/중간자 시도를 한 번 더 차단합니다. 운영 환경에서는 CA 신뢰 설정, 호스트명 검증과 인증서 교체 시의 동작도 함께 확인해야 합니다.

참조