설명
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 신뢰 설정, 호스트명 검증과 인증서 교체 시의 동작도 함께 확인해야 합니다.