콘텐츠로 이동

보안 및 운영

이 브리지는 사용자의 HTML을 네트워크 경계 너머의 브라우저 엔진으로 전송합니다. 이 페이지는 그 경계를 방어하는 모든 컨트롤을 소스 기준으로 설명합니다. 컨트롤이 표준을 인용하는 경우, 그 인용은 코드 자체의 docblock이 선언하는 내용입니다. 이 페이지는 코드가 주장하는 내용을 다시 설명할 뿐, 규범적 문구를 재구성하지 않습니다.

패키지 자체의 docblock은 방어 대상 위협을 다음과 같이 명시합니다:

  • XSS-to-PDF — 렌더링 중에 실행되는 악의적 마크업.
  • SSRF — 내부 주소로 요청을 유도하는 마크업 또는 대상 URL.
  • 리소스 고갈 — 과도하게 큰 입력 또는 압축 폭탄.
  • DNS 리바인딩 — 검증은 통과하지만 연결 시점에 사설 주소로 해석되는 호스트명.
  • 경로상의 TLS 가로채기 — Worker로 가는 경로에서 교체된 인증서.

각 항목은 아래의 구체적이고 테스트 가능한 컨트롤로 대응합니다.

입력 컨트롤(요청이 PHP를 떠나기 전)

섹션 제목: “입력 컨트롤(요청이 PHP를 떠나기 전)”

CloudflareSecurityPolicy::validate()는 요청이 구성되기 전에 실행됩니다:

컨트롤동작제한 출처
크기 상한maxHtmlSize보다 큰 HTML을 거부합니다CloudflareRendererConfig, 기본값 5000000바이트
Base64 압축 폭탄 가드모든 data:…;base64,… URI의 디코딩 크기를 추정하여, 상한에 도달하거나 초과하면 거부합니다MAX_DATA_URI_BYTES = 13631488
Meta-refresh 금지대소문자를 구분하지 않고 모든 <meta http-equiv="refresh">를 거부합니다CloudflareSecurityPolicy의 정규식

위반이 발생하면 위반 값과 제한을 명시한 메시지와 함께 RuntimeException이 발생합니다. meta-refresh 금지가 필요한 이유는 refresh 디렉티브가 Worker가 렌더링하는 페이지 내부에서 내비게이션을 유도할 수 있기 때문입니다. 이는 URL이 아니라 콘텐츠 안에 존재하는 SSRF 벡터입니다.

nextpdf/core의 HTML 보안 정책(HtmlSecurityPolicyInterface, 기본값 DefaultHtmlSecurityPolicy)은 파싱 계층에서 동작하며, 위의 전송 계층 검사를 보완합니다. 이 정책은 getHtmlSecurityPolicy()로 조회합니다. 사용자 정의 정책은 생성자를 통해 주입합니다.

대상 컨트롤(SSRF 및 DNS 리바인딩)

섹션 제목: “대상 컨트롤(SSRF 및 DNS 리바인딩)”

CloudflareSecurityPolicy::validateWorkerUrl():

  1. 파싱되지 않거나 scheme/host가 없는 URL을 거부합니다(Invalid Worker URL).
  2. HTTPS가 아닌 모든 스킴을 거부합니다(Worker URL must use HTTPS).
  3. IP 리터럴 호스트의 경우, 다음을 사용해 사설 또는 예약 범위를 거부합니다 PHP의 FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE. 실제로 이는 RFC 1918 사설 공간, 루프백, RFC 3927 링크 로컬 주소를 거부합니다. 테스트는 192.168.x, 127.0.0.1, 169.254.x의 거부를 명시적으로 검사합니다. 범위 소속 여부는 이 패키지가 고정한 조항이 아니라 PHP의 필터 확장에 따라 결정됩니다. RFC 1918과 RFC 3927은 여기서 해당 범위의 잘 알려진 정의로서 설명을 위해 언급됩니다.
  4. 호스트명의 경우, dns_get_record()를 통해 모든 A 및 AAAA 레코드를 해석합니다(첫 번째 응답만 반환하는 gethostbyname()가 아닙니다). 해석된 주소 중 어느 하나라도 사설 또는 예약 주소이면 거부합니다.

모든 레코드를 해석하는 것은 의도적인 동작이며, 클래스 docblock에 방어 수단으로 문서화되어 있습니다. 단일 레코드 조회가 공개 주소를 선택하더라도 이후 연결이 사설 주소를 선택할 수 있는, 여러 레코드를 반환하는 호스트를 방어하기 위한 것입니다. 이는 OWASP SSRF Prevention Cheat Sheet와 일치하며, 해당 문서는 애플리케이션이 도메인 이름 뒤에 있는 모든 IP 주소(A 및 AAAA 레코드)를 가져와 그 모두에 비공개 주소 검사를 적용하도록 안내합니다.

validateWorkerUrl()는 검증된 IP 집합을 반환합니다. 그런 다음 렌더러는 전송 직전에 assertPinsStillValid()를 호출합니다. 이 호출은 호스트를 다시 해석하고, 검증 이후 새 IP가 나타났으면 거부합니다(Worker URL DNS answer changed since validation — possible DNS rebinding attack). 이는 검증과 연결 사이의 점검 시점/사용 시점(time-of-check / time-of-use) 구간을 닫습니다.

검증된 IP 집합 또는 SPKI 고정 집합이 있고 또한 PSR-17 ResponseFactory가 제공된 경우, 렌더러는 주입된 PSR-18 클라이언트 대신 Transport\PinnedCurlTransport를 사용합니다. 이 전송은 cURL 핸들 계층에서 다음을 강제합니다:

  • 고정 DNSCURLOPT_RESOLVE는 host:port를 검증된 IP 집합에 바인딩하므로 libcurl이 연결 시점에 자체 조회를 수행하지 않습니다. 이것이 사용자 영역의 DNS 검사가 실제로 연결을 바인딩하게 만드는 요소입니다. 이 설정이 없으면 libcurl이 다른 주소를 해석할 수 있습니다.
  • TLS 공개 키 고정CURLOPT_PINNEDPUBLICKEY가 결합된 고정 집합에서 설정됩니다. 이는 RFC 7469 §2.6을 따릅니다. 서버가 제시한 SPKI 지문 집합이 구성된 고정 집합과 교집합을 이룰 때 고정된 연결이 수락되며, 고정 검증 실패는 복구 불가능한 것으로 취급됩니다. 고정 문자열은 sha256/<base64>에서 cURL의 sha256//<base64> 형식으로 정규화됩니다. 잘못된 형식의 고정은 InvalidSpkiPinException을 발생시킵니다.
  • TLS 검증 활성화CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2.
  • 자동 리다이렉트 없음CURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 0. 3xx는 libcurl이 검증되지 않은 호스트로 따라가지 않고 정책 계층에 노출됩니다. 클래스 docblock은 리다이렉트가 조용히 따라가지 않고 재검증되도록 하는 의도적 선택이라고 명시합니다.
  • 하드 타임아웃CURLOPT_TIMEOUTrenderTimeout에서 설정됩니다(기본값 30초).

cURL 오류가 발생하거나 본문이 문자열이 아니면 cURL 오류 번호 및 메시지와 함께 CloudflareRenderException을 발생시킵니다.

구성은 pinnedPublicKeys와 별도의 backupPublicKeys를 포함합니다. RFC 7469 §2.5는 백업 고정(오프라인으로 보관된, 아직 배포되지 않은 보조 키 쌍의 지문)을 우발적인 고정 검증 실패에서 복구하는 주된 방법으로 설명합니다. 인증서 교체가 엔드포인트를 망가뜨리지 않도록 최소 하나의 백업 고정을 유지하는 것은 그 지침을 따르는 조치입니다. 별도의 필드는 교체를 독립적으로 검증할 수 있게 합니다. 운영 시:

  • 교체를 직접 제어할 수 있는 리프 또는 중간 인증서의 SPKI를 고정합니다.
  • 교체하기 전에 항상 다음 인증서에 대한 백업 고정을 구성합니다.
  • 빈 고정 집합은 고정을 비활성화합니다. 안정적이고 알려진 인증서 체인에서만 사용하십시오. 고정은 구성으로 옵트인하는 방식입니다.
  • Worker 요청은 Authorization: Bearer <apiToken>를 포함합니다. apiToken#[SensitiveParameter]이므로 스택 트레이스에서 가려집니다. 도달성 프로브는 HTTP HEAD에서 동일한 Bearer 헤더를 전송합니다.
  • R2 액세스 키(accessKeyId, secretAccessKey)는 #[SensitiveParameter]이며 AWS Signature V4 서명 키를 파생하는 데만 사용됩니다.
  • ApiKeyValidatorhash_equals()(타이밍 안전)로 키를 비교하고, validateHashed()를 통해 SHA-256 해시 키 저장을 지원합니다.
  • 구성 객체는 final readonly입니다. 한 번 설정된 시크릿은 변경할 수 없습니다.
  • 시크릿은 환경 변수 또는 시크릿 관리자에서 가져옵니다. 절대 커밋하지 마십시오. 이 패키지는 더 넓은 NextPDF 보안 기준선을 따릅니다: PHPStan Level 10, 모든 파일에 declare(strict_types=1), eval()/exec() 미사용, SHA로 고정된 GitHub Actions.
  • Cloudflare 플랫폼 제한(Worker CPU 시간, 메모리, 요청 본문 상한, 또는 서브요청 수)은 명시하지 않습니다. 이 문서가 명시하는 유일한 크기 및 시간 제한은 패키지 자체가 강제하는 항목이며, 위와 /integrations/cloudflare/configuration/.에 나열되어 있습니다. 플랫폼 제한은 Cloudflare의 공식 문서와 사용자 Worker의 자체 구현을 참조하십시오.
  • PDF에 서명하지 않으며 서명 적합성에 대해 어떠한 주장도 하지 않습니다. 서명이 필요한 경우, 여기서 렌더링한 다음 서명 엔진으로 서명하십시오. NextPDF Pro는 PAdES B-B 서명만 제공합니다. 장기 검증 프로파일은 Enterprise 기능이며 이 브리지의 범위를 벗어납니다.
  • 파이프라인을 인증하거나 보증하거나 “변조 불가”로 만들지 않습니다. 이 페이지에 설명된 구체적이고 소스로 검증 가능한 컨트롤을 구현할 뿐, 그 이상은 아무것도 구현하지 않습니다.
증상첫 번째 확인 사항
Worker URL must use HTTPS구성된 workerUrl 스킴.
private or reserved IPWorker 호스트명의 DNS 레코드. RFC 1918 / 루프백 / RFC 3927 공간으로 해석되는 레코드.
DNS answer changed since validationDNS 불안정 또는 리바인딩 시도. 다시 해석하여 레코드 집합을 점검하십시오.
cURL transport error네트워크 경로, TLS 체인, 그리고 고정이 설정된 경우 제공된 인증서의 SPKI가 여전히 고정 집합에 있는지 여부.
인증서 교체 직후 렌더링 실패일치하는 백업 고정이 없는 고정 집합. 교체하기 전에 새 SPKI 를 백업으로 추가하십시오.
is not installed / no LocalRendererFactoryInterface폴백은 활성화되었지만 팩토리가 연결되지 않았거나, nextpdf/artisan이 없습니다.
노드 간 속도 제한 거부가 일관되지 않음메모리 내 제한기는 프로세스별로 동작합니다. 공유 저장소로 앞단을 구성하십시오.

취약점은 GitHub Security Advisories 또는 저장소 SECURITY.md의 보안 연락처를 통해 보고하십시오. 보안 이슈를 공개 GitHub 이슈로 등록해서는 안 됩니다.

  • /integrations/cloudflare/overview/ — 패키지가 이 경계를 중심으로 구성된 이유.
  • /integrations/cloudflare/configuration/ — 고정 집합 및 제한 필드.
  • /integrations/cloudflare/troubleshooting/ — 전체 실패-예외 매핑.