Skip to content

보안 모범 사례

이 가이드는 프로덕션 환경에서 TCPDF-Next를 배포하기 위한 실행 가능한 보안 권장 사항을 제공합니다. 이 사례를 따르면 PDF 생성 파이프라인이 엔터프라이즈 보안 표준을 충족하도록 보장할 수 있습니다.

입력 검증 (writeHtml 전 HTML 정리)

사용자 제공 HTML에서 PDF를 생성할 때는 HTML 렌더러에 전달하기 전에 항상 입력을 정리하세요. TCPDF-Next의 HtmlRenderer는 HTML을 충실하게 파싱하고 렌더링하므로, 정리하지 않으면 악성 마크업이 악용될 수 있습니다.

php
use YeeeFang\TcpdfNext\Html\HtmlRenderer;

// 위험: 원시 사용자 입력을 직접 전달하지 마세요
// $renderer->writeHtml($userInput);

// 안전: 전용 라이브러리로 먼저 정리
$clean = \HTMLPurifier::getInstance()->purify($userInput);
$renderer->writeHtml($clean);

핵심 규칙:

  • 렌더링 전에 <script>, <iframe>, <object>, <embed>, <link> 태그를 제거합니다.
  • hrefsrc 속성에서 javascript:data: URI 스킴을 제거합니다.
  • 레이아웃에 필요한 CSS 속성만 허용합니다(position: fixed 없음, CSS 값에 url() 없음).
  • 문자 인코딩을 검증합니다 — 렌더러에 전달하기 전에 입력이 유효한 UTF-8인지 확인합니다.

인증서 관리 (안전한 저장 및 교체)

저장소 계층

방법보안 수준사용 사례
하드웨어 보안 모듈 (HSM)최고엔터프라이즈, 규제 산업
클라우드 KMS (AWS KMS, Azure Key Vault, GCP KMS)높음클라우드 네이티브 배포
강력한 암호구문을 가진 PKCS#12 파일중간소규모 배포
PEM 파일 (암호화됨)중-하개발, 테스트
PEM 파일 (암호화되지 않음)최저프로덕션에서 절대 사용 금지

교체 정책

  • 만료 최소 30일 전에 인증서를 갱신합니다.
  • 30일, 14일, 7일, 1일 임계값에서 자동 알림으로 만료를 모니터링합니다.
  • 손상된 인증서는 발급 CA를 통해 즉시 해지합니다.
  • 모든 인증서 수명주기 작업에 대한 서명된 감사 로그를 유지합니다.
php
use YeeeFang\TcpdfNext\Certificate\CertificateStore;

$store = new CertificateStore();
$store->loadFromDirectory('/etc/tcpdf-next/certs/', '*.pem');

$activeCert = $store->getActiveCertificate('document-signing');

if ($activeCert->getExpirationDate() < new \DateTimeImmutable('+30 days')) {
    $logger->warning('Signing certificate expires soon', [
        'subject' => $activeCert->getSubject(),
        'expires' => $activeCert->getExpirationDate()->format('Y-m-d'),
    ]);
}

DANGER

개인 키를 소스 코드 저장소, 공유 파일시스템의 암호화되지 않은 파일, 저장 시 암호화 없는 데이터베이스 컬럼, 로그 파일에 절대 저장하지 마세요.

비밀번호 처리 (SASLprep 및 강력한 비밀번호)

PDF 암호화 비밀번호를 설정할 때, 플랫폼 간 일관된 비밀번호 처리를 보장하기 위해 SASLprep(RFC 4013)을 통한 유니코드 정규화를 적용합니다:

php
// TCPDF-Next는 비밀번호에 SASLprep을 자동으로 적용합니다
$pdf->setEncryption()
    ->setAlgorithm(EncryptionAlgorithm::AES256)
    ->setUserPassword('pässwörd-with-ünïcöde')  // SASLprep이 내부적으로 정규화
    ->setOwnerPassword($strongOwnerPassword)
    ->apply();

비밀번호 정책 권장사항:

  • 사용자 비밀번호는 최소 12자, 소유자 비밀번호는 최소 20자.
  • 소유자 비밀번호에는 암호학적으로 안전한 난수 생성기(random_bytes())를 사용합니다.
  • 소스 코드에 비밀번호를 하드코딩하지 않습니다 — 환경 변수 또는 시크릿 관리자에서 로드합니다.
  • 사용 후 sodium_memzero()로 메모리에서 비밀번호를 삭제합니다.

SSRF 방지 (이미지, TSA, OCSP의 URL 검증)

TCPDF-Next는 기본적으로 SSRF를 차단하지만, 정당한 외부 리소스에 대한 허용 목록을 구성해야 합니다:

php
use YeeeFang\TcpdfNext\Security\NetworkPolicy;

$networkPolicy = NetworkPolicy::create()
    ->denyPrivateNetworks()     // 10.x, 172.16.x, 192.168.x 차단
    ->denyLoopback()            // 127.0.0.1 차단
    ->denyLinkLocal()           // 169.254.x 차단
    ->allowDomain('cdn.yourcompany.com')       // 이미지
    ->allowDomain('timestamp.digicert.com')    // TSA
    ->allowDomain('ocsp.digicert.com')         // OCSP
    ->setMaxRedirects(3)
    ->setRequestTimeout(10);

$pdf = PdfDocument::create()
    ->setNetworkPolicy($networkPolicy)
    ->build();

체크리스트:

  • 가져오기 전에 모든 URL을 검증합니다(스킴, 호스트, 포트).
  • TSA 및 OCSP 응답자 도메인을 명시적으로 허용 목록에 추가합니다.
  • file://, gopher://, ftp:// 및 기타 비-HTTP(S) 스킴을 차단합니다.
  • 보안 모니터링을 위해 차단된 모든 요청을 로깅합니다.

파일 경로 검증 (경로 탐색 방지)

사용자 제공 파일 경로(예: 폰트 파일, 이미지, 출력 대상)를 받을 때:

php
use YeeeFang\TcpdfNext\Security\ResourcePolicy;

$resourcePolicy = ResourcePolicy::strict()
    ->allowLocalDirectory('/app/public/assets/')
    ->allowLocalDirectory('/app/storage/fonts/')
    ->denyAllRemote();

$pdf = PdfDocument::create()
    ->setResourcePolicy($resourcePolicy)
    ->build();

규칙:

  • 사용자 입력을 파일 경로에 직접 연결하지 않습니다.
  • 경로를 절대 정규 형식으로 해석하고 허용된 디렉토리에 대해 검증합니다.
  • .., 널 바이트 또는 인쇄 불가능한 문자가 포함된 경로를 거부합니다.
  • 프로덕션에서는 ResourcePolicy::strict()를 사용합니다 — 기본적으로 모든 접근을 거부합니다.

배포 보안 (Docker 및 파일 권한)

Docker 구성

dockerfile
FROM php:8.5-fpm-alpine

# 비루트 사용자로 실행
RUN addgroup -S tcpdf && adduser -S tcpdf -G tcpdf
USER tcpdf

# 위험한 PHP 함수 비활성화
RUN echo "disable_functions = exec,passthru,shell_exec,system,proc_open,popen" \
    >> /usr/local/etc/php/conf.d/security.ini

# 읽기 전용 파일시스템 (쓰기 가능한 볼륨은 명시적으로 마운트)
# docker run --read-only --tmpfs /tmp ...

파일 권한

bash
# 인증서 디렉토리: 웹 서버 사용자만 읽을 수 있음
chown -R www-data:www-data /etc/tcpdf-next/certs/
chmod 700 /etc/tcpdf-next/certs/
chmod 600 /etc/tcpdf-next/certs/*.p12
chmod 600 /etc/tcpdf-next/certs/*.pem

# 출력 디렉토리: 웹 서버 사용자만 쓸 수 있음
chown -R www-data:www-data /var/lib/tcpdf-next/output/
chmod 700 /var/lib/tcpdf-next/output/

# 임시 디렉토리: 쓰기 가능하되 전체 읽기 불가
chown -R www-data:www-data /tmp/tcpdf-next/
chmod 700 /tmp/tcpdf-next/

브라우저에 표시되는 PDF를 위한 콘텐츠 보안 정책

브라우저에서 PDF를 인라인으로 제공할 때, 임베딩 공격을 방지하기 위해 적절한 HTTP 헤더를 설정합니다:

php
return response($pdf->toString(), 200, [
    'Content-Type' => 'application/pdf',
    'Content-Disposition' => 'inline; filename="document.pdf"',
    'Content-Security-Policy' => "default-src 'none'; plugin-types application/pdf",
    'X-Content-Type-Options' => 'nosniff',
    'X-Frame-Options' => 'DENY',
    'Cache-Control' => 'no-store, no-cache, must-revalidate',
]);

민감한 데이터가 포함된 PDF의 경우, 브라우저 내 렌더링 대신 다운로드를 강제하기 위해 Content-Disposition: attachment를 사용하는 것이 좋습니다.

감사 로깅 권장사항

보안에 민감한 모든 PDF 작업에 대한 포괄적인 감사 로깅을 구성합니다:

php
use YeeeFang\TcpdfNext\Security\AuditLogger;

AuditLogger::configure([
    'channel' => 'tcpdf-security',
    'log_signing' => true,
    'log_encryption' => true,
    'log_validation' => true,
    'log_key_access' => true,
    'log_tsa_requests' => true,
    'log_resource_access' => true,   // 이미지/폰트 로딩 로깅
    'log_blocked_requests' => true,  // SSRF 차단 로깅
    'redact_sensitive' => true,      // 로그에서 비밀번호/키 편집
]);

모니터링 항목:

  • 실패한 서명 검증 시도 (문서 변조 가능성).
  • 인증서 만료 경고.
  • TSA 통신 실패.
  • 비정상적인 서명 볼륨 (키 손상 가능성).
  • 차단된 SSRF 시도 (공격 탐색 가능성).
  • 예상치 못한 경로에서의 리소스 로딩.

PDF 권한에 대한 최소 권한 원칙

PDF 문서 권한을 설정할 때 필요한 최소한의 접근만 부여합니다:

php
use YeeeFang\TcpdfNext\Encryption\Permissions;

// 제한적: 읽기 전용 문서
$pdf->setEncryption()
    ->setPermissions(Permissions::ACCESSIBILITY)  // 스크린 리더 접근만
    ->setUserPassword('reader')
    ->setOwnerPassword($strongOwnerPassword)
    ->apply();

// 보통: 인쇄 가능한 문서
$pdf->setEncryption()
    ->setPermissions(
        Permissions::PRINT_HIGH_QUALITY
        | Permissions::ACCESSIBILITY
    )
    ->apply();

// 피해야 함: 모든 권한을 부여하면 암호화의 목적이 무의미해집니다
// Permissions::ALL은 사용 가능하지만 거의 사용하지 않아야 합니다

권한 가이드라인:

  • 수신자가 문서를 편집해야 하는 경우가 아니면 MODIFY_CONTENTS를 부여하지 않습니다.
  • 스크린 리더 호환성을 위해 항상 ACCESSIBILITY를 부여합니다(많은 관할 지역에서 법적 요구사항).
  • 특별한 이유가 없다면 PRINT_LOW_QUALITY 대신 PRINT_HIGH_QUALITY를 사용합니다.
  • 부여된 각 권한의 근거를 코드에 문서화합니다.

더 읽을거리

LGPL-3.0-or-later 라이선스로 배포됩니다.