기술 문서

Delphi 애플리케이션에서 PDFium Component를 사용한 안전한 PDF 미리보기

자체 애플리케이션 내에서 신뢰할 수 없는 PDF를 미리 보는 것은 실행과 관련된 결정이며, 중요한 부분은 뷰어의 외관(chrome)이 아니라 미리보기 창이 자체적으로 수행하지 않도록 거부하는 항목입니다. 파일을 디스크에 쓰지 마세요. 파일의 링크가 셸(shell)을 실행하지 못하게 하세요. 첨부 파일에 경로를 부여하지 마세요. 악의적인 문서로 인한 피해의 대부분은 엔진 악용이 아니라 공격자가 제공한 입력을 뷰어가 완벽하게 일반적인 방식으로 처리할 때 발생합니다. 즉, NTLM 자격 증명을 유출하는 UNC 공유에 대한 file:// 링크 열기, 임시 디렉터리에 복사본 남기기, 포함된 페이로드를 파일 이름 문자열이 지시하는 곳으로 복사하기 등입니다. PDFium Component는 Delphi, C++Builder 및 Lazarus를 위한 소스 코드 PDF 뷰어로, 이러한 관련 스위치들을 사용자가 제어할 수 있는 위치에 배치합니다. 스크립팅을 차단하는 로드 시간 플래그, 사용자가 거부할 수 있는 링크 클릭 이벤트, 자체 코드를 통해 실행되는 첨부 파일 액세스, 읽을 수 있는 권한 비트(permission bits)를 제공합니다. 아래 순서는 문서가 도착하는 순간부터 사용자가 문서 안의 무언가를 클릭하는 순간까지를 따릅니다

미리보기 창의 위협 모델

어느 수준까지 "안전한 미리보기"가 보장되는지 정직하게 파악하세요. 렌더러는 당신이 무엇을 하든 신뢰할 수 없는 바이트를 구문 분석하며, 엔진 자체의 강화(hardening)는 당신이 서 있는 바닥입니다. 그 바닥 위의 모든 것은 애플리케이션 정책입니다. 즉, 스크립트가 초기화되는지, 링크 클릭이 수행하는 작업은 무엇인지, 포함된 파일이 디스크에 도달할 수 있는지, 클립보드와 프린터가 문인지 벽인지 등입니다. 초기에 제외해야 할 한 가지는 엔진의 FPDF_SetSandBoxPolicy 스위치입니다. 대부분의 엔진 제한은 컴파일되어 있으며 이 스위치는 실제로 변경되는 것이 거의 없고, 여기에 격리(isolation) 예산을 할당하는 것은 단지 무언가를 했다는 잘못된 안도감만 낳을 뿐입니다. 공용 업로드 포털과 같이 입력이 진정으로 악의적인 경우 유일한 실제 격리는 별도의 권한이 낮은 프로세스에서 렌더링하고 비트맵을 UI로 보내는 것입니다. 프로세스 내 플래그는 정책일 뿐, 봉쇄(containment)가 아닙니다

사용자가 전혀 클릭하지 않아 잊기 쉬운 두 가지 표면이 있습니다. 첫 번째는 임시 파일입니다. 파이프라인이 미리보기 전에 인바운드 문서를 디스크에 스테이징하는 경우, 무언가 확실하게 해당 복사본을 삭제하지 않는 한 해당 세션 이후에도 복사본이 유지되며, "임시 디렉터리에서 복구할 수 있는" 파일은 창 자체가 적용하는 모든 제어 수단을 조용히 무력화한 것입니다. 악의적인 바이트가 자신의 경로를 가지지 않도록 TPdfStreamAdapter를 통해 메모리에서 로드하세요. 두 번째는 클립보드입니다. 선택 및 복사를 허용하는 미리보기는 한 번에 한 화면씩 문서를 이미 내보낸 것이며, 어떤 링크 가로채기도 이를 잡지 못합니다

UI가 아닌 로드 시점에 JavaScript 비활성화

PDFium Component의 문서 JavaScript는 양식 채우기(form-fill) 환경과 함께만 초기화됩니다. 따라서 FormFill := False로 로드하면 겉으로 나타나는 증상만 억제하는 대신 스크립팅을 근본적으로 비활성화합니다

procedure TPreviewPane.LoadUntrusted(const FilePath: string);
begin
  Pdf.FileName := FilePath;
  Pdf.FormFill := False;     // no form environment, hence no JavaScript engine
  Pdf.Active := True;

  FPermissions := Pdf.Permissions;   // raw flag word; all bits set = unrestricted
end;

이러한 절충은 현실적이며 애플리케이션 사양에 반영되어야 합니다. 양식 채우기가 비활성화되면 합법적인 AcroForm 상호 작용 및 유효성 검사 스크립트도 사라지며, 필드는 마지막으로 저장된 모양으로 렌더링되지만 편집할 수는 없습니다. 미리보기는 채우기가 아닌 보기를 의미하므로, 미리보기 창에서는 보통 이것이 올바른 선택입니다. 그러나 동일한 창이 신뢰할 수 있는 내부 문서의 양식 채우기 표면으로도 사용되는 경우, 정답은 악의적인 사례에는 너무 느슨하고 신뢰할 수 있는 사례에는 너무 엄격한 절충 설정으로 하나의 경로를 만드는 것이 아니라 두 경로 사이에 명시적인 신뢰 결정을 두는 것입니다. 이러한 분할에서 양식 채우기 측면의 함정은 양식 필드 탐색 및 외관 재생성에서 다룹니다

링크: 셸(shell)을 실행하는 기본 핸들러

그대로 두면 링크 클릭은 바로 운영 체제로 전달됩니다. 뷰어의 기본 LinkOptions에는 loAutoOpenURI가 포함되며, 이는 UNC 공유 유출을 일으킬 수 있는 file:// 링크입니다. 두 가지 이벤트가 초크 포인트(choke point)를 형성합니다. 페이지 텍스트에서 감지된 URL에 대한 OnWebLinkClick과, URI 또는 실행 작업을 포함하는 링크 주석에 대한 OnAnnotationLinkClick입니다. 무언가를 결정하기 전에 두 이벤트 모두에서 조건 없이 Handled := True를 설정한 다음, 정책이 허용하는 항목만 다시 허용하세요. 두 번째 방어 계층으로, 악의적인 입력의 경우 LinkOptions에서 loAutoOpenURI를 제거하고 기본적으로 꺼져 있는 loAutoLaunch가 복사된 구성을 통해 다시 슬그머니 들어오지 않도록 하세요

procedure TPreviewPane.PdfViewWebLinkClick(Sender: TObject;
  const Url: WString; var Handled: Boolean);
begin
  Handled := True;   // never fall through to the default shell behavior

  if (AnsiStartsText('https://', Url) or AnsiStartsText('http://', Url))
    and HostIsAllowed(Url) then
    OpenInBrowser(Url)
  else
    FAudit.LogBlockedLink(FDocumentId, Url);
end;

이것이 실제로 유효한지 결정하는 두 가지 세부 사항이 있습니다. 첫 번째, 스키마 검사는 구문 분석 전의 원시 문자열에 대한 접두사 검사(prefix check)여야 합니다. file://, UNC 경로 및 특이한 스키마는 순진한 URL 파서를 충돌시키거나 너무 성급하게 정규화하는 파서를 통과하는 바로 그 값들이기 때문입니다. 두 번째, 모든 차단은 문서 ID를 첨부하여 로그에 기록하세요. 소수의 file:// 링크 차단은 백그라운드 노이즈에 불과하지만, 짧은 시간 내에 여러 인바운드 문서에서 발생하는 일련의 차단은 보안 팀이 다른 곳보다 당신으로부터 먼저 듣고 싶어 할 사건입니다

첨부 파일: 확장자 정책과 당신이 선택하지 않은 파일 이름

PDF는 컨테이너이며, AttachmentName[] 속성이 포함된 AttachmentCount는 무언가가 디스크에 닿기 전에 무엇이 포함되어 있는지 알려줍니다. 여기서 두 가지 개별 제어 수단이 중요하며, 그중 하나만 명백합니다. 명백한 것은 유형 정책입니다. 즉, 내보낼 수 있는 확장자의 허용 목록(allowlist)입니다. 포착하기 어려운 제어 수단은 첨부 파일의 이름이 전적으로 공격자가 제어할 수 있는 데이터라는 점입니다. ..\..\Startup\update.exe와 같은 포함된 이름은 부주의한 저장을 경로 탐색(path traversal)으로 바꾸어 로그인 시 Windows가 실행하는 폴더에 실행 파일을 남길 수 있습니다. 이 컴포넌트는 Attachment[]를 통해 페이로드를 바이트로 전달하고 코드가 경로를 선택하도록 하므로, 원시 포함된 문자열이 아니라 정리된(sanitized) 기본 이름(basename)에서 해당 경로를 구성하세요

procedure TPreviewPane.ExportAttachment(Index: Integer; const TargetDir: string);
var
  RawName, SafeName, Ext: string;
  Data: TBytes;
begin
  RawName := string(Pdf.AttachmentName[Index]);
  SafeName := ExtractFileName(RawName);    // strips any path components
  Ext := LowerCase(ExtractFileExt(SafeName));

  if not FAllowedExt.Contains(Ext) then    // allowlist, not blocklist
    raise EPreviewPolicy.CreateFmt('Attachment type %s blocked by policy', [Ext]);

  Data := Pdf.Attachment[Index];           // embedded payload as raw bytes
  TFile.WriteAllBytes(
    IncludeTrailingPathDelimiter(TargetDir) + SafeName, Data);
end;

허용 목록(allowlist) 방향을 선호하세요. "위험한" 확장자 차단 목록은 들어본 적도 없는 확장자가 무기화되는 날 지는 경주입니다. .pdf, .png.csv의 허용 목록은 문제 발생 시 폐쇄적으로 실패(fail closed)합니다

암호화 권한이 실제로 약속하는 것

ISO 32000-1의 표준 보안 핸들러는 인쇄, 콘텐츠 복사 및 수정에 대한 권한 플래그를 인코딩하며, PermissionsUserPermissions 속성은 문서가 열리면 이를 원시 비트마스크(bitmask)로 표시합니다. ISO 32000-1 표 22는 비트를 정의하며 암호화되지 않은 파일은 모든 비트가 설정된 것으로 보고합니다. 명령 계층에서 이를 읽고 준수하되, 이것이 무엇인지 명확히 하세요. 소유자 암호와 빈 사용자 암호로 암호화된 문서의 경우, 열 때 콘텐츠가 완전히 해독되며 이 플래그는 강제 메커니즘이 아니라 호환 뷰어에 대한 요청일 뿐입니다. 여기에는 두 가지 결과가 따르며, 그들은 반대 방향으로 작용합니다. 사용자에게 권한 플래그를 그들이 받는 문서의 보안 속성으로 제시하지 마세요. 그것은 보안 속성이 아니기 때문입니다. 동시에 일반 복사(비트 5)가 거부되더라도 접근성 추출 비트(비트 10)는 준수하세요. 스크린 리더 액세스는 보안상의 이점 없이 보조 기술을 차단하지 않도록 의도적으로 권한 모델에서 별도로 분리되었습니다

거부된 작업은 툴바 버튼을 숨기는 것이 아니라 명령 수준에서 강제 적용하세요. Ctrl+C, 컨텍스트 메뉴 및 드래그 앤 드롭 선택은 모두 툴바를 우회하며, 복사 명령 내의 단일 권한 검사만이 아무것도 우회하지 않습니다

사용자 암호가 필요한 문서의 경우 Active := True 이전에 Password를 할당하고 이 값을 비밀로 취급하세요. 즉, 세션당 자격 증명 저장소에서 가져오고, 로그나 충돌 보고서에 포함하지 않으며, 문서 옆에 절대 보관하지 마세요. "편의를 위해" 암호를 캐시하는 미리보기 창은 조용히 아무런 보호 장치가 없는 암호 데이터베이스가 되었습니다

인쇄는 복사 규칙의 설정을 상속받기보다는 자체적인 결정이 필요합니다. 물리적 인쇄물은 정의상 감사(audit)되지 않지만 인쇄를 전면적으로 차단하면 사용자는 스크린샷으로 향하게 되며 이는 모든 측면에서 더 나쁩니다. 흔히 선택하는 중간 지점은 인쇄를 허용하되 각 페이지에 사용자 ID와 타임스탬프를 스탬프 처리하는 것이며, 인쇄 명령 내부에서 적용됩니다. 이에 대해 올바른 기대치를 가지세요. 워터마크는 억지력(deterrence)과 귀속(attribution)일 뿐 예방(prevention)은 아닙니다

인테이크(intake) 과정에서 이미 알려줬어야 하는 사항

미리보기 창은 파일이 암호화 여부, JavaScript 존재 여부, 첨부 파일 조사, 양식 유형 등 프로파일 정보와 함께 도착할 때 더 나은 결정을 내립니다. 이러한 검사 과정은 뷰어의 업스트림에 위치해야 하며, PDF 인테이크 검토 워크벤치 구축 패턴은 미리보기 정책에서 소비하고자 하는 정확한 플래그를 생성합니다. 인테이크 과정에서 위험하다고 표시된 파일은 자동으로 강화된 경로를 통해 열리고, 일반적인 문서는 편의성을 유지합니다. 두 단계를 각각의 구성 화면이 아닌 하나의 공유 정책 객체에 연결하세요. 두 번의 릴리스가 지나면 처음 얼마나 신중하게 작성했는지에 상관없이 설정이 엇갈리게 됩니다

프로세스 내(in-process)와 프로세스 외부(out-of-process) 사이의 경계선은 문서를 보내는 사람이 누구인지에 따라 다릅니다. 일반적인 비즈니스 인테이크의 경우, 문서를 보내는 사람은 아는 사람이고 단지 부주의한 것일 뿐이므로 스크립팅을 끄고 링크를 가로채는 프로세스 내 미리보기는 방어할 수 있는 기준입니다. 익명 공용 업로드의 경우 그렇지 않으며 어떠한 양의 프로세스 내 플래그 설정으로도 기준을 충족할 수 없습니다. 별도의 권한이 낮은 워커(worker)에서 이를 렌더링하고 비트맵을 UI로 보내 엔진의 결함으로 인해 호스트 애플리케이션 대신 워커만 손상되게 하세요. 이러한 분할을 신중하게 결정하고 각 수집 경로가 어느 버킷(bucket)에 해당하는지 기록해 두세요. 잘못 추측했을 때의 비용이 비대칭적이기 때문입니다

라이선스, 보안 관련 API 표면, 그리고 강화된 뷰어 데모는 제품 페이지인 PDFium Component에 있습니다