🔒 보안

PCI DSS

Payment Card Industry Data Security Standard (결제 카드 산업 데이터 보안 표준)

PCI DSS는 신용카드, 직불카드 등 결제 카드 데이터를 보호하기 위한 글로벌 보안 표준입니다. Visa, Mastercard, American Express 등 주요 카드 브랜드가 공동 설립한 PCI SSC(Security Standards Council)에서 관리하며, 카드 데이터를 저장, 처리, 전송하는 모든 조직에 적용됩니다.

📖 상세 설명

PCI DSS(Payment Card Industry Data Security Standard)는 2004년 Visa, Mastercard, American Express, Discover, JCB의 5개 주요 카드 브랜드가 공동으로 제정한 결제 카드 보안 표준입니다. 이전에는 각 카드사가 개별 보안 기준을 운영했으나, 통합된 글로벌 표준의 필요성이 대두되어 PCI SSC(Security Standards Council)가 설립되었습니다. 현재 버전은 PCI DSS v4.0(2022년 발표)이며, v4.0.1로 업데이트되었습니다.

PCI DSS의 핵심은 CHD(Cardholder Data)와 SAD(Sensitive Authentication Data)의 보호입니다. CHD는 PAN(Primary Account Number, 카드번호), 카드 소유자 이름, 만료일, 서비스 코드를 포함합니다. SAD는 풀 마그네틱 스트라이프 데이터, CVV/CVC, PIN을 포함하며, 승인 후 저장이 금지됩니다. 이 데이터가 처리되는 환경을 CDE(Cardholder Data Environment)라 하며, PCI DSS 컴플라이언스의 핵심 범위입니다.

표준은 6개 목표 아래 12개 요구사항으로 구성됩니다: (1) 방화벽 설치 및 유지, (2) 기본 비밀번호 변경, (3) 저장된 카드 데이터 보호, (4) 전송 중 암호화, (5) 안티바이러스 소프트웨어, (6) 보안 시스템 개발, (7) 접근 제한, (8) 고유 ID 부여, (9) 물리적 접근 제한, (10) 모니터링 및 로깅, (11) 정기 테스트, (12) 정보보안 정책. 각 요구사항에는 세부 통제 항목이 있어 총 400개 이상의 점검 항목이 있습니다.

PCI DSS 준수는 카드 거래량에 따라 4개 레벨로 구분됩니다. Level 1(연간 600만 건 이상)은 QSA(Qualified Security Assessor)의 현장 심사가 필수입니다. Level 2-4는 SAQ(Self-Assessment Questionnaire)로 자가 평가가 가능하지만, 취급하는 데이터 유형에 따라 SAQ A, A-EP, B, C, D 등 적합한 양식을 선택해야 합니다. 분기별 ASV(Approved Scanning Vendor) 취약점 스캔도 필수입니다. 미준수 시 카드사로부터 거래 정지, 과징금, 데이터 유출 시 막대한 보상 책임을 질 수 있습니다.

💻 코드 예제

# PCI DSS 준수를 위한 카드 데이터 처리 - Python
import re
import hashlib
import logging
from datetime import datetime
from typing import Optional, Dict
from cryptography.fernet import Fernet
from functools import wraps

# PCI DSS 요구사항 10: 로깅 설정
logging.basicConfig(
    filename='pci_audit.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
audit_logger = logging.getLogger('PCI_AUDIT')


class PANValidator:
    """PAN(Primary Account Number) 검증 및 보호 클래스"""

    # 카드 브랜드별 BIN 패턴 (처음 6자리)
    CARD_PATTERNS = {
        'visa': r'^4[0-9]{12}(?:[0-9]{3})?$',
        'mastercard': r'^5[1-5][0-9]{14}$|^2[2-7][0-9]{14}$',
        'amex': r'^3[47][0-9]{13}$',
        'discover': r'^6(?:011|5[0-9]{2})[0-9]{12}$',
        'jcb': r'^(?:2131|1800|35\d{3})\d{11}$'
    }

    @staticmethod
    def luhn_check(pan: str) -> bool:
        """Luhn 알고리즘으로 PAN 유효성 검증"""
        digits = [int(d) for d in pan if d.isdigit()]
        odd_sum = sum(digits[-1::-2])
        even_sum = sum(sum(divmod(2 * d, 10)) for d in digits[-2::-2])
        return (odd_sum + even_sum) % 10 == 0

    @staticmethod
    def identify_brand(pan: str) -> Optional[str]:
        """카드 브랜드 식별"""
        clean_pan = re.sub(r'\D', '', pan)
        for brand, pattern in PANValidator.CARD_PATTERNS.items():
            if re.match(pattern, clean_pan):
                return brand
        return None

    @staticmethod
    def validate(pan: str) -> Dict:
        """PAN 종합 검증"""
        clean_pan = re.sub(r'\D', '', pan)
        return {
            'valid_length': 13 <= len(clean_pan) <= 19,
            'valid_luhn': PANValidator.luhn_check(clean_pan),
            'brand': PANValidator.identify_brand(clean_pan)
        }


class PCICompliantStorage:
    """PCI DSS 준수 카드 데이터 저장소

    요구사항 3: 저장된 카드홀더 데이터 보호
    """

    def __init__(self, encryption_key: bytes):
        self.cipher = Fernet(encryption_key)

    def mask_pan(self, pan: str) -> str:
        """PAN 마스킹 (요구사항 3.4)

        처음 6자리(BIN)와 마지막 4자리만 표시 가능
        예: 4111111111111111 -> 411111******1111
        """
        clean_pan = re.sub(r'\D', '', pan)
        if len(clean_pan) < 13:
            raise ValueError("Invalid PAN length")
        return f"{clean_pan[:6]}{'*' * (len(clean_pan) - 10)}{clean_pan[-4:]}"

    def encrypt_pan(self, pan: str) -> bytes:
        """PAN 암호화 (요구사항 3.5)

        AES-256 이상 강도의 암호화 필수
        """
        clean_pan = re.sub(r'\D', '', pan)
        encrypted = self.cipher.encrypt(clean_pan.encode())

        audit_logger.info(f"PAN encrypted: last4={clean_pan[-4:]}")
        return encrypted

    def hash_pan(self, pan: str) -> str:
        """PAN 해싱 (검색용, 단방향)

        SHA-256 이상 권장, 솔트 추가
        """
        clean_pan = re.sub(r'\D', '', pan)
        salt = "PCI_DSS_SALT_DO_NOT_SHARE"  # 실제로는 안전하게 관리
        return hashlib.sha256(f"{salt}{clean_pan}".encode()).hexdigest()

    def truncate_pan(self, pan: str) -> str:
        """PAN 자르기 (마지막 4자리만 저장)

        암호화 없이 저장 가능한 형태
        """
        clean_pan = re.sub(r'\D', '', pan)
        return clean_pan[-4:]


class SADProtection:
    """SAD(Sensitive Authentication Data) 보호

    요구사항 3.2: 승인 후 SAD 저장 금지
    - 풀 트랙 데이터 (마그네틱 스트라이프)
    - CVV/CVC/CID
    - PIN/PIN 블록
    """

    FORBIDDEN_FIELDS = ['cvv', 'cvc', 'cvv2', 'cvc2', 'cid', 'pin',
                        'track1', 'track2', 'magnetic_stripe']

    @classmethod
    def validate_no_sad(cls, data: Dict) -> bool:
        """SAD 데이터 저장 시도 탐지"""
        for field in cls.FORBIDDEN_FIELDS:
            if field in data or field.lower() in [k.lower() for k in data.keys()]:
                audit_logger.critical(
                    f"SAD STORAGE ATTEMPT BLOCKED: field={field}"
                )
                raise ValueError(
                    f"PCI DSS Violation: Storing {field} is prohibited after authorization"
                )
        return True

    @classmethod
    def scrub_sad(cls, data: Dict) -> Dict:
        """응답 데이터에서 SAD 제거"""
        scrubbed = data.copy()
        for field in cls.FORBIDDEN_FIELDS:
            scrubbed.pop(field, None)
            # 대소문자 무관하게 제거
            for key in list(scrubbed.keys()):
                if key.lower() == field.lower():
                    del scrubbed[key]
        return scrubbed


def pci_audit_log(action: str):
    """PCI DSS 요구사항 10: 감사 로그 데코레이터"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            user_id = kwargs.get('user_id', 'system')
            timestamp = datetime.utcnow().isoformat()

            audit_logger.info(
                f"ACTION_START: action={action}, user={user_id}, time={timestamp}"
            )

            try:
                result = func(*args, **kwargs)
                audit_logger.info(
                    f"ACTION_SUCCESS: action={action}, user={user_id}"
                )
                return result
            except Exception as e:
                audit_logger.error(
                    f"ACTION_FAILED: action={action}, user={user_id}, error={str(e)}"
                )
                raise
        return wrapper
    return decorator


# 사용 예시
if __name__ == "__main__":
    # 암호화 키 생성 (실제로는 HSM 또는 KMS 사용)
    key = Fernet.generate_key()
    storage = PCICompliantStorage(key)

    test_pan = "4111111111111111"

    # PAN 검증
    validation = PANValidator.validate(test_pan)
    print(f"PAN 검증: {validation}")

    # 마스킹
    masked = storage.mask_pan(test_pan)
    print(f"마스킹: {masked}")

    # 암호화
    encrypted = storage.encrypt_pan(test_pan)
    print(f"암호화: {encrypted[:50]}...")

    # 해싱
    hashed = storage.hash_pan(test_pan)
    print(f"해시: {hashed}")

    # SAD 검증
    payment_data = {
        'pan': test_pan,
        'expiry': '12/25',
        'cvv': '123'  # 이것은 저장하면 안 됨!
    }

    try:
        SADProtection.validate_no_sad(payment_data)
    except ValueError as e:
        print(f"SAD 위반 탐지: {e}")
// PCI DSS 준수 토큰화 서비스 - Node.js
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');

/**
 * PCI DSS 요구사항 3 준수를 위한 토큰화 서비스
 * 실제 PAN을 토큰으로 대체하여 CDE 범위 최소화
 */
class TokenizationService {
    constructor() {
        // 토큰-PAN 매핑 저장소 (실제로는 HSM + 암호화된 DB)
        this.tokenVault = new Map();
        this.encryptionKey = crypto.randomBytes(32);
        this.algorithm = 'aes-256-gcm';
    }

    /**
     * PAN을 토큰으로 변환
     * @param {string} pan - 카드번호
     * @returns {object} - 토큰 및 마스킹된 PAN
     */
    tokenize(pan) {
        const cleanPan = pan.replace(/\D/g, '');

        // Luhn 검증
        if (!this.luhnCheck(cleanPan)) {
            throw new Error('Invalid PAN: Luhn check failed');
        }

        // 기존 토큰 확인 (중복 방지)
        for (const [token, encrypted] of this.tokenVault) {
            const decrypted = this.decrypt(encrypted);
            if (decrypted === cleanPan) {
                return {
                    token,
                    maskedPan: this.maskPan(cleanPan),
                    last4: cleanPan.slice(-4)
                };
            }
        }

        // 새 토큰 생성 (포맷 보존 옵션)
        const token = this.generateToken(cleanPan);

        // PAN 암호화 후 저장
        const encrypted = this.encrypt(cleanPan);
        this.tokenVault.set(token, encrypted);

        console.log(`[AUDIT] PAN tokenized: last4=${cleanPan.slice(-4)}, token=${token.slice(0, 8)}...`);

        return {
            token,
            maskedPan: this.maskPan(cleanPan),
            last4: cleanPan.slice(-4)
        };
    }

    /**
     * 토큰을 PAN으로 복원 (권한 있는 시스템만)
     * @param {string} token - 토큰
     * @param {string} purpose - 복원 목적 (감사용)
     * @returns {string} - 원본 PAN
     */
    detokenize(token, purpose) {
        const encrypted = this.tokenVault.get(token);
        if (!encrypted) {
            throw new Error('Token not found');
        }

        const pan = this.decrypt(encrypted);

        console.log(`[AUDIT] PAN detokenized: token=${token.slice(0, 8)}..., purpose=${purpose}`);

        return pan;
    }

    /**
     * 포맷 보존 토큰 생성 (FPE 스타일)
     * 실제로는 BIN(처음 6자리) + 마지막 4자리 유지 가능
     */
    generateToken(pan) {
        // 옵션 1: UUID 기반 (완전 랜덤)
        // return uuidv4().replace(/-/g, '').slice(0, 16);

        // 옵션 2: 포맷 보존 (BIN + 랜덤 + last4)
        const bin = pan.slice(0, 6);
        const last4 = pan.slice(-4);
        const middle = crypto.randomBytes(3).toString('hex').slice(0, 6);
        return `${bin}${middle}${last4}`;
    }

    /**
     * AES-256-GCM 암호화
     */
    encrypt(plaintext) {
        const iv = crypto.randomBytes(16);
        const cipher = crypto.createCipheriv(this.algorithm, this.encryptionKey, iv);

        let encrypted = cipher.update(plaintext, 'utf8', 'hex');
        encrypted += cipher.final('hex');
        const authTag = cipher.getAuthTag();

        return JSON.stringify({
            iv: iv.toString('hex'),
            authTag: authTag.toString('hex'),
            data: encrypted
        });
    }

    /**
     * AES-256-GCM 복호화
     */
    decrypt(encryptedJson) {
        const { iv, authTag, data } = JSON.parse(encryptedJson);

        const decipher = crypto.createDecipheriv(
            this.algorithm,
            this.encryptionKey,
            Buffer.from(iv, 'hex')
        );
        decipher.setAuthTag(Buffer.from(authTag, 'hex'));

        let decrypted = decipher.update(data, 'hex', 'utf8');
        decrypted += decipher.final('utf8');

        return decrypted;
    }

    /**
     * PAN 마스킹 (요구사항 3.4)
     */
    maskPan(pan) {
        return `${pan.slice(0, 6)}${'*'.repeat(pan.length - 10)}${pan.slice(-4)}`;
    }

    /**
     * Luhn 알고리즘 검증
     */
    luhnCheck(pan) {
        let sum = 0;
        let isEven = false;

        for (let i = pan.length - 1; i >= 0; i--) {
            let digit = parseInt(pan[i], 10);

            if (isEven) {
                digit *= 2;
                if (digit > 9) digit -= 9;
            }

            sum += digit;
            isEven = !isEven;
        }

        return sum % 10 === 0;
    }
}

/**
 * PCI DSS 요구사항 4: 전송 중 암호화 검증 미들웨어
 */
function requireTLS(req, res, next) {
    // X-Forwarded-Proto 헤더 또는 직접 연결 확인
    const isSecure = req.secure ||
        req.headers['x-forwarded-proto'] === 'https' ||
        req.connection.encrypted;

    if (!isSecure && process.env.NODE_ENV === 'production') {
        console.log(`[AUDIT] INSECURE_CONNECTION_BLOCKED: ip=${req.ip}`);
        return res.status(403).json({
            error: 'PCI DSS Requirement 4: TLS required for cardholder data transmission'
        });
    }

    // TLS 버전 확인 (1.2 이상 필수)
    const tlsVersion = req.connection.getPeerCertificate?.()?.version;
    if (tlsVersion && parseFloat(tlsVersion) < 1.2) {
        return res.status(403).json({
            error: 'PCI DSS: TLS 1.2 or higher required'
        });
    }

    next();
}

// 사용 예시
const tokenService = new TokenizationService();

// 토큰화
const result = tokenService.tokenize('4111111111111111');
console.log('토큰화 결과:', result);

// 디토큰화 (결제 처리 시에만)
const pan = tokenService.detokenize(result.token, 'payment_processing');
console.log('복원된 PAN:', tokenService.maskPan(pan));

module.exports = { TokenizationService, requireTLS };
-- PCI DSS 준수를 위한 SQL 데이터 보호 기법

-- =====================================================
-- 요구사항 3: 저장된 카드홀더 데이터 보호
-- =====================================================

-- 1. 카드 데이터 테이블 구조 (올바른 설계)
CREATE TABLE payment_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    token VARCHAR(32) NOT NULL UNIQUE,          -- 토큰 (PAN 대체)
    pan_hash VARCHAR(64) NOT NULL,              -- SHA-256 해시 (검색용)
    pan_last4 CHAR(4) NOT NULL,                 -- 마지막 4자리 (표시용)
    bin CHAR(6),                                -- BIN (처음 6자리, 선택적)
    card_brand VARCHAR(20),                     -- Visa, Mastercard 등
    expiry_month SMALLINT,                      -- 만료 월 (암호화 가능)
    expiry_year SMALLINT,                       -- 만료 년
    cardholder_name_encrypted BYTEA,            -- 암호화된 카드소유자명
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- CVV/CVC는 절대 저장하지 않음! (SAD)
    -- PIN도 절대 저장하지 않음! (SAD)

    CONSTRAINT valid_expiry_month CHECK (expiry_month BETWEEN 1 AND 12),
    CONSTRAINT valid_last4 CHECK (pan_last4 ~ '^[0-9]{4}$')
);

-- 2. 암호화된 PAN 저장소 (별도 보안 DB/HSM 권장)
CREATE TABLE pan_vault (
    token VARCHAR(32) PRIMARY KEY REFERENCES payment_tokens(token),
    encrypted_pan BYTEA NOT NULL,               -- AES-256 암호화된 PAN
    key_id VARCHAR(36) NOT NULL,                -- 암호화 키 식별자
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- 접근 감사용 컬럼
    last_accessed_at TIMESTAMP,
    access_count INTEGER DEFAULT 0
);

-- 3. PAN 마스킹 함수
CREATE OR REPLACE FUNCTION mask_pan(pan TEXT)
RETURNS TEXT AS $$
BEGIN
    -- 처음 6자리 + 마스크 + 마지막 4자리
    RETURN SUBSTRING(pan, 1, 6) ||
           REPEAT('*', LENGTH(pan) - 10) ||
           SUBSTRING(pan, LENGTH(pan) - 3);
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- 예시: SELECT mask_pan('4111111111111111');
-- 결과: 411111******1111

-- 4. PAN 해싱 함수 (검색용)
CREATE OR REPLACE FUNCTION hash_pan(pan TEXT, salt TEXT DEFAULT 'PCI_SALT')
RETURNS TEXT AS $$
BEGIN
    RETURN encode(sha256((salt || pan)::bytea), 'hex');
END;
$$ LANGUAGE plpgsql IMMUTABLE;


-- =====================================================
-- 요구사항 7: 카드홀더 데이터 접근 제한
-- =====================================================

-- 1. 역할 기반 접근 제어 (RBAC)
CREATE ROLE pci_readonly;
CREATE ROLE pci_payment_processor;
CREATE ROLE pci_admin;

-- 읽기 전용 역할: 마스킹된 데이터만 조회 가능
GRANT SELECT (id, token, pan_last4, card_brand, expiry_month, expiry_year)
ON payment_tokens TO pci_readonly;

-- 결제 처리 역할: PAN vault 접근 가능
GRANT SELECT ON pan_vault TO pci_payment_processor;
GRANT EXECUTE ON FUNCTION mask_pan TO pci_payment_processor;

-- 관리자: 전체 접근 (감사 추적 필수)
GRANT ALL ON payment_tokens TO pci_admin;
GRANT ALL ON pan_vault TO pci_admin;


-- =====================================================
-- 요구사항 10: 네트워크 리소스 및 CHD 접근 추적
-- =====================================================

-- 1. 감사 로그 테이블
CREATE TABLE pci_audit_log (
    id BIGSERIAL PRIMARY KEY,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    user_name VARCHAR(100) NOT NULL,
    client_ip INET,
    action VARCHAR(50) NOT NULL,            -- SELECT, INSERT, UPDATE, DELETE
    table_name VARCHAR(100) NOT NULL,
    record_id UUID,
    column_accessed TEXT[],                 -- 접근한 컬럼 목록
    query_text TEXT,                        -- 실행된 쿼리 (마스킹된)
    success BOOLEAN NOT NULL,
    error_message TEXT
);

-- 2. 자동 감사 로그 트리거
CREATE OR REPLACE FUNCTION log_pan_access()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO pci_audit_log (
        user_name, client_ip, action, table_name, record_id, success
    ) VALUES (
        current_user,
        inet_client_addr(),
        TG_OP,
        TG_TABLE_NAME,
        COALESCE(NEW.id, OLD.id),
        TRUE
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER audit_pan_vault_access
AFTER SELECT OR INSERT OR UPDATE OR DELETE ON pan_vault
FOR EACH ROW EXECUTE FUNCTION log_pan_access();

-- 3. 민감 데이터 접근 시 알림
CREATE OR REPLACE FUNCTION alert_on_bulk_access()
RETURNS TRIGGER AS $$
DECLARE
    access_count INTEGER;
BEGIN
    -- 지난 1분간 동일 사용자의 접근 횟수 확인
    SELECT COUNT(*) INTO access_count
    FROM pci_audit_log
    WHERE user_name = current_user
    AND timestamp > NOW() - INTERVAL '1 minute'
    AND table_name = 'pan_vault';

    -- 임계값 초과 시 경고 (대량 데이터 추출 시도 탐지)
    IF access_count > 100 THEN
        RAISE WARNING 'PCI ALERT: Unusual bulk access detected for user %', current_user;
        -- 실제로는 SIEM으로 알림 전송
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;


-- =====================================================
-- 요구사항 3.2: SAD 저장 방지 제약조건
-- =====================================================

-- SAD 컬럼이 추가되는 것을 방지하는 체크
-- (실제로는 DDL 트리거로 구현)

-- 금지된 컬럼명 목록
CREATE TABLE forbidden_columns (
    column_name VARCHAR(50) PRIMARY KEY
);

INSERT INTO forbidden_columns VALUES
    ('cvv'), ('cvc'), ('cvv2'), ('cvc2'), ('cid'),
    ('pin'), ('pin_block'),
    ('track1'), ('track2'), ('magnetic_stripe'),
    ('full_track_data');

-- DDL 이벤트 트리거 (PostgreSQL 9.3+)
CREATE OR REPLACE FUNCTION prevent_sad_columns()
RETURNS event_trigger AS $$
DECLARE
    obj record;
BEGIN
    FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands()
    LOOP
        IF obj.command_tag IN ('CREATE TABLE', 'ALTER TABLE') THEN
            -- 금지된 컬럼명 체크 로직
            RAISE NOTICE 'DDL command detected: %', obj.command_tag;
        END IF;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- CREATE EVENT TRIGGER check_sad_columns
-- ON ddl_command_end
-- EXECUTE FUNCTION prevent_sad_columns();

🗣️ 실무에서 이렇게 말하세요

💬 결제 시스템 설계 회의에서
"CDE 범위를 최소화하는 것이 핵심입니다. 우리 서버에서 PAN을 직접 처리하지 말고, PCI 인증된 결제 게이트웨이의 iFrame이나 JavaScript SDK를 사용합시다. 그러면 SAQ A 레벨로 컴플라이언스 범위가 줄어들고, 토큰만 저장하면 됩니다. CVV는 절대 저장하지 마세요. 승인 후 저장은 PCI DSS 위반입니다."
💬 PCI DSS 감사 대응에서
"저희는 Level 2 가맹점으로 SAQ D를 작성했습니다. 분기별 ASV 스캔 결과와 연간 침투 테스트 보고서가 있습니다. PAN은 AES-256으로 암호화되어 저장되고, 키는 HSM에서 관리합니다. 네트워크 세그멘테이션을 통해 CDE를 분리했고, 접근 로그는 1년간 보관합니다. 요구사항 10 충족을 위한 SIEM 대시보드를 보여드릴까요?"
💬 개발팀에 보안 요구사항 전달 시
"결제 페이지는 반드시 TLS 1.2 이상으로만 접근 가능하게 하세요. 카드 입력 폼은 PCI 인증된 결제사의 호스티드 필드를 사용하고, 우리 서버로는 토큰만 전송됩니다. 로그에 PAN이 찍히지 않도록 마스킹 필터를 적용하고, 에러 메시지에도 카드번호가 노출되면 안 됩니다. 코드 리뷰 시 이 부분을 특히 확인해주세요."

⚠️ 주의사항 & 베스트 프랙티스

CVV/PIN 저장 (SAD 저장 금지 위반)

CVV, CVC, PIN, 풀 트랙 데이터는 승인 후 절대 저장하면 안 됩니다. 메모리에서도 즉시 삭제해야 합니다. 이 규칙을 어기면 카드사로부터 거래 정지, 막대한 과징금, 데이터 유출 시 전체 손해배상 책임을 집니다.

로그에 PAN 기록

디버그 로그, 에러 로그, 액세스 로그에 카드번호가 평문으로 기록되면 PCI DSS 위반입니다. 모든 로깅 포인트에 PAN 마스킹 필터를 적용하세요. 카드번호 패턴(16자리 숫자)을 정규식으로 탐지해 자동 마스킹하는 것이 좋습니다.

CDE 범위 확대

PAN이 경유하는 모든 시스템이 CDE에 포함됩니다. 범위가 넓어질수록 컴플라이언스 비용이 기하급수적으로 증가합니다. 토큰화, P2PE(Point-to-Point Encryption), 호스티드 결제 페이지를 활용해 CDE를 최소화하세요.

PCI DSS 베스트 프랙티스

PCI 인증된 결제 서비스(Stripe, Adyen 등) 사용, 토큰화로 PAN 직접 저장 회피, 네트워크 세그멘테이션으로 CDE 격리, HSM 기반 키 관리, 분기별 취약점 스캔(ASV), 연간 침투 테스트, 직원 보안 교육을 실행하세요. SAQ 유형을 신중히 선택해 범위를 최적화하세요.

🔗 관련 용어

📚 더 배우기