💡 전체 작동 예제가 GitHub에 있습니다:
신뢰할 수 없는 문서 안전하게 로드 파이썬
이전 방식은 고통스러웠다
업로드된 문서의 썸네일을 렌더링하기 위해 세 줄의 코드를 작성했습니다. 코드는 다음과 같았으며, 정상적으로 보였습니다:
with signature.Signature(upload_path) as sign:
save_page_preview(sign, thumbnail_path)
GroupDocs.Signature 26.9 이전에 이 코드가 수행한 작업은 문서가 가리키는 모든 주소를 가져오는 것이었습니다. Word 파일은 실제로 포함되지 않은 그림을 보유할 수 있습니다—파일에 URL이 저장되고, 이를 여는 프로그램은 해당 URL을 다운로드합니다. 데스크톱에서는 이것이 기능이지만, 업로드를 받는 서버에서는 파일을 보낸 사람이 인프라가 요청할 주소를 결정한다는 의미가 됩니다.
이 공격은 서버 측 요청 위조(SSRF)라고 불리며, 세 가지 형태가 있습니다. 인터넷에서 접근할 수 없는 내부 주소가 서버에서는 접근 가능하므로, 조작된 문서는 http://169.254.169.254/ 같은 주소나 로컬호스트의 관리자 엔드포인트를 요청할 수 있습니다. UNC 경로는 Windows 호스트가 외부 인증을 시도하게 만들어 공격자가 제어하는 서버에 자격 증명을 전달하게 할 수 있습니다. 그리고 응답이 전혀 없는 호스트에 대한 링크는 로딩 스레드를 타임아웃될 때까지 대기하게 만들어, 무해해 보이는 문서들로 작업자 풀을 소모시키는 저비용 방법이 됩니다.
문서 라이브러리 자체에 버그가 있는 것은 아닙니다. 링크를 따라가는 것이 포맷이 요구하는 동작이며, 불편했던 점은 이러한 동작이 기본값으로 설정돼 있었고 코드 리뷰에서도 눈에 띄지 않았다는 것입니다.
더 나은 방법이 있습니다
안전한 문서 로딩은 Python용 GroupDocs.Signature가 이러한 요청을 거부하도록 동작합니다. 26.9 버전부터 LoadOptions.skip_external_resources 기본값이 True로 설정되어, 동일한 세 줄의 코드가 이제 아무것도 가져오지 않고 연결된 그림이 있을 경우 자리 표시자를 렌더링합니다.
이 변경은 새로운 기능이라기보다 기본값이 바뀐 것입니다—속성 자체는 이미 존재했습니다. 26.9에서 바뀐 점은 코드에서 별도로 지정하지 않을 경우 어떤 동작을 할지 결정하는 기본값이 바뀐 것입니다. 대부분의 서비스가 사용하는 유일한 설정이 바로 이것입니다.
새로운 방법: 세 가지 로드 모드
1단계 - 신뢰할 수 없는 모든 것에 대해 기본값 유지
LoadOptions를 전혀 사용하지 않습니다:
with signature.Signature(source_path) as sign:
return save_page_preview(sign, preview_path)
아무 요청도 발생하지 않습니다. 프리뷰는 원래보다 작게 생성되며, 이 크기 차이가 요청이 전혀 이루어지지 않았다는 가장 직관적인 증거가 됩니다.
2단계 - 실제로 소유한 호스트를 화이트리스트에 추가
많은 문서가 합법적인 위치(회사 CDN, 내부 이미지 서버, 템플릿 저장소 등)를 가리킵니다. 해당 호스트만 허용하고 나머지는 차단합니다:
load_options = LoadOptions()
load_options.whitelisted_resources = [trusted_address]
with signature.Signature(source_path, load_options) as sign:
return save_page_preview(sign, preview_path)
매칭 규칙에 주의해야 합니다. 이는 리소스 주소에 대해 대소문자를 구분하지 않는 부분 문자열 검사이므로, 짧은 조각만으로도 위험할 수 있습니다: github는 github.attacker.example/payload.png와도 일치합니다. 스킴, 호스트, 경로 전체를 지정하세요—예시에서는 raw.githubusercontent.com/groupdocs-signature/를 화이트리스트에 추가합니다.
3단계 - 모든 것을 허용, 의도적으로
26.9 이전 동작을 그대로 사용하려면 다음과 같이 설정합니다:
load_options = LoadOptions()
load_options.skip_external_resources = False
자체 애플리케이션이 만든 문서에 대해 합리적입니다. 한 가지 함정은 오래된 load_external_resources 속성이 반대 의미를 갖고 있다는 점입니다. 따라서 skip_external_resources = False는 load_external_resources = True를 대체합니다. 오래된 속성에서 값을 복사하면 보안 설정이 뒤바뀌지만 오류 메시지는 표시되지 않습니다.
나란히 비교: 이전 vs. 이후
동일한 문서, 동일한 코드 경로, 세 가지 로드 정책을 적용한 결과입니다. 아래는 샘플 Result/ 폴더에 커밋된 파일 크기이며, 직접 확인할 수 있습니다:
| 로드 모드 | 프리뷰 크기 | 외부 요청 |
|---|---|---|
| 기본값 (26.9 이후) | 16,435 바이트 | 없음 |
| 화이트리스트된 호스트 | 51,738 바이트 | 허용된 주소에 1회 |
| 모든 리소스 (26.9 이전 기본값) | 51,738 바이트 | 링크된 리소스당 1회 |
링크된 그림이 차이의 35,303 바이트를 차지합니다. 설정값을 직접 확인하기 전까지는 신뢰하기 어려웠으며, 여러분도 동일하게 속성을 읽어 실제 적용된 구성을 확인하시기 바랍니다.
외부 리소스로 간주되는 것은 무엇인가요?
사람들이 생각하는 것보다 범위가 좁습니다. 외부 리소스는 링크된 그림, INCLUDEPICTURE 필드, 프레젠테이션·스프레드시트의 링크된 그림, 그리고 SVG가 참조하는 이미지·스타일시트입니다. 임베디드된 콘텐츠는 파일 내부에 이미 존재하므로 요청이 필요하지 않아 영향을 받지 않습니다.
이 구분이 바로 보안 경계 전체입니다. 문서가 바이트 대신 주소를 저장할 때만 서버가 외부에 접근하게 되므로, 전체 코퍼스에서 얼마나 많은 파일이 임베드 대신 링크를 사용하는지만 확인하면 됩니다. 링크가 전혀 없다면 새로운 기본값은 비용이 들지 않으며, 추가 검토 없이 바로 업그레이드할 수 있습니다.
실제 예시: 서명되는 업로드
기본값 변경이 존재하는 이유가 바로 이 경우입니다. 외부에서 문서를 받아 서명을 추가해야 할 때:
with signature.Signature(source_path) as sign:
options = QrCodeSignOptions("Approved by GroupDocs.Signature")
options.encode_type = QrCodeTypes.QR
options.left = 400
options.top = 50
options.width = 120
options.height = 120
result = sign.sign(output_path, options)
문서가 로드, 서명, 저장되는 동안 외부 리소스가 요청되지 않습니다. 서명된 출력 파일은 여전히 링크를 유지하므로, 사용자가 나중에 Word에서 열면 자신의 머신에서 그림이 해석됩니다. 스킵은 서버 측 정책이며 문서 자체를 수정하는 것이 아니라, 타인의 파일을 다룰 때 안전하게 적용할 수 있는 이유입니다.
업그레이드 시 다른 변경 사항은?
대부분의 서비스에서는 눈에 보이는 변화가 없습니다. 이는 보안 기본값이 전역 동작을 바꾸는 경우 업그레이드 검토를 통과하기 어렵기 때문입니다. 서명, 검증, 검색 기능은 그대로 유지됩니다. 차이가 나는 경우는 프리뷰가 링크된 그림 대신 자리 표시자를 보여주는 경우이며, 이는 의도된 동작입니다. 호스트가 여러분 소유라면 화이트리스트에 추가하고, 그렇지 않다면 그대로 두세요.
별도로 강조할 점은 SVG입니다. SVG는 URL을 통해 이미지와 스타일시트를 참조할 수 있으며, 이러한 참조도 동일한 외부 리소스 규칙에 적용됩니다. SVG는 흔히 업로드되는 포맷이면서 SSRF 벡터이기도 합니다. 서버 측에서 SVG 아바타를 렌더링하는 서비스는 바로 이 변경이 보호하는 대상입니다.
파이썬 세부 사항: 프리뷰가 어떻게 작성되는가
PreviewOptions는 경로 대신 두 개의 스트림 팩토리를 받으며, 일반 파이썬 호출 가능 객체만 있으면 됩니다:
def create_page_stream(page_data):
return open(preview_path, "wb")
def release_page_stream(page_data, page_stream):
page_stream.close()
preview_options = PreviewOptions(create_page_stream, release_page_stream)
preview_options.preview_format = PreviewOptions.PreviewFormats.PNG
sign.generate_preview(preview_options)
하나의 페이지당 스트림을 생성하고, 다른 하나는 이를 해제합니다. 샘플 문서는 한 페이지이므로 하나의 파일이 작성됩니다; 다중 페이지 입력의 경우 파일명에 페이지 번호를 포함하거나, 각 페이지가 이전 파일을 덮어쓰게 해야 합니다.
결론
기본값이 바뀌어 위험한 동작은 명시적인 결정이 필요하고, 안전한 동작은 아무 설정도 하지 않아도 적용됩니다. 신뢰할 수 없는 입력에는 기본값을 유지하고, 자체 호스트가 관여되는 경우에만 좁게 화이트리스트를 설정하세요. 서명 작업 자체는 네트워크를 전혀 사용하지 않음을 기억하십시오.
파일 크기 외에 더 강력한 검증이 필요하다면, 테스트 문서를 직접 제어하는 호스트에 연결하고 프리뷰가 실행되는 동안 접근 로그를 확인해 보세요. 크기는 바이트가 도착했는지 알려주고, 접근 로그는 요청 자체가 있었는지 알려줍니다. 두 결과가 일치하지 않을 때가 바로 중요한 경우이며, 접근이 불가능한 화이트리스트 호스트는 차단된 호스트와 출력만으로는 구분할 수 없습니다.
샘플을 자체 문서에 한 번 실행해 보면 1분 정도 소요되며, 세 가지 파일 크기로 서비스가 파일을 로드하면서 어떤 외부 요청을 했는지 정확히 파악할 수 있습니다.