기술 문서

HotXLS에서 신뢰할 수 없는 EMF와 WMF 안전하게 재생

Excel 통합 문서는 EMF와 WMF 그림을 실을 수 있고, 이를 그리는 관례적 방법은 바이트 스트림을 운영체제 메타파일 플레이어에 넘기는 것입니다. 이 결정은 직접 들여다볼 가치가 있습니다. 메타파일은 그래픽스 API용 직렬화된 명령 스트림이며, 재생한다는 것은 이메일로 도착한 파일이 그래픽스 드라이버를 운전하게 두는 것입니다. HotXLS는 다른 길을 택합니다. XLSDecodeVectorScene이 메타파일을 자체 해석해 헤더, 모든 레코드 크기, 선언된 레코드 총수, 파일 끝 레코드의 정확한 위치를 검증하고, escape 레코드를 완전히 거부하며, Canvas와 SVG 백엔드가 자기 코드로 재생하는 프리미티브 드로잉 명령의 TXLSVectorScene을 돌려줍니다. 어느 시점에도 드라이버 재생은 개입하지 않습니다

HotXLS가 XLSDecodeVectorScene으로 신뢰할 수 없는 EMF와 WMF 워크시트 바이트를 GDI 메타파일 재생 대신 TXLSVectorScene 명령 목록으로 해석
HotXLS는 메타파일을 자체 해석해 Canvas와 SVG 재생용 프리미티브 명령을 돌려줍니다. 관례적 경로는 그래픽스 스택 위에서 바이트 스트림을 실행합니다

이 거래는 격리를 위한 커버리지입니다. 사각형 지향 명령 화이트리스트는 디자이너가 만들 수 있는 모든 메타파일을 재현하지 못하므로, 장면은 표현하지 못한 드로잉 레코드의 개수를 보고하고 호출자가 그에 대한 처분을 정합니다. 자신이 만들지 않은 문서를 렌더링하는 서버 프로세스라면 그 거래가 올바른 방향입니다

메타파일 재생은 신뢰할 수 없는 입력에 왜 부적합한가

포맷이 그림이 아니라 프로그램이기 때문입니다. EMF 레코드 스트림은 디바이스 컨텍스트 상태 스택을 조작하고, 핸들 테이블에서 오브젝트를 할당·선택하며, 페이로드가 디바이스 드라이버로 전달되는 escape 레코드를 실을 수 있습니다. 재생은 메타파일이 같은 기계의 협조적인 애플리케이션에서 왔다는 가정 위에 작성된 플랫폼 그래픽스 스택의 경로들을 구동합니다. 입력이 스프레드시트 첨부파일이라면 그 가정은 사라지며, 스프레드시트 라이브러리 안의 어떤 주의도 도움이 되지 않습니다. 파싱하는 컴포넌트가 라이브러리가 아니기 때문입니다

이것은 컨테이너 계층을 지배하는 것과 같은 논리입니다. 통합 문서는 ZIP 아카이브이며, ZIP 중앙 디렉터리 끝 검증 문서에서 설명하듯 HotXLS는 선언된 오프셋을 믿는 대신 중앙 디렉터리를 검증합니다. 메타파일 페이로드는 같은 문제의 다음 계층입니다

디코더가 무엇이든 그리기 전에 검사하는 것

검증은 구조적이며 미리 일어납니다. 그리기 시작하면서 가면서 검증하는 파서는 이미 검증하지 않은 데이터 위에서 행동한 것이기 때문입니다. 헤더는 그럴듯하게가 아니라 엄격하게 일치해야 합니다. 모든 레코드는 남은 버퍼 안에 들어가고 자기 고정 필드에는 충분한 크기를 선언해야 합니다. 헤더가 선언하는 레코드 수는 실제 존재하는 레코드와 일치해야 합니다. 파일 끝 레코드는 근처 어딘가가 아니라 스트림이 끝나는 바로 그 자리에 있어야 하며, 이것이 유효한 그림 뒤에 두 번째 페이로드를 숨기는 꼬리 쓰레기 트릭을 막습니다

구조를 넘어 디코더는 의미론에서 fail-closed입니다. escape 레코드는 건너뛰어지는 것이 아니라 거부됩니다. 디코더가 모델링하지 않는 상태 변경 레코드는 무시되는 대신 디코딩 실패를 만듭니다. 상태 변경을 무시한다는 것은 이후의 모든 드로잉 명령이 파일이 요청하지 않은 상태에서 실행됨을 뜻하고, 결과는 아무도 예측할 수 없는 방식으로 틀린 그림입니다. 지원 명령 집합 밖의 드로잉 레코드는 다른 문제입니다. 그것들은 세어지고 건너뛰어집니다. 빠진 도형은 조용한 손상이 아니라 보이고 보고 가능한 간극이기 때문입니다

XLSDecodeVectorScene이 헤더, 레코드 크기, 총수, EOF 위치를 미리 검사한 뒤 escape 레코드를 거부하고 지원 밖 드로잉 레코드를 세는 모습
구조 검사는 미리 돌고 fail-closed 의미론은 escape 레코드를 거부하며, 지원 밖 드로잉 레코드는 세고 건너뛰기만 합니다

예산은 포맷 계약의 일부입니다

벡터 포맷에는 압축 해제 폭탄의 자기 버전이 있습니다. 몇 킬로바이트의 레코드가 수억 점의 폴리라인을 선언하거나, 선언된 치수가 곱해지면 테라바이트가 되는 이미지를 담을 수 있습니다. 따라서 상한은 기계가 우연히 견디는 것이 아니라 명시적 상수여야 합니다

// lxVectorScene에서: 암묵이 아니라 명시된 디코딩 예산
XL_VECTOR_MAX_RECORDS           = 1000000;
XL_VECTOR_MAX_HANDLES           = 4096;
XL_VECTOR_MAX_DC_DEPTH          = 32;
XL_VECTOR_MAX_COMMANDS          = 100000;
XL_VECTOR_MAX_POINTS_PER_RECORD = 100000;
XL_VECTOR_MAX_TOTAL_POINTS      = 2000000;
XL_VECTOR_MAX_TEXT_CHARS        = 4096;
XL_VECTOR_MAX_TOTAL_TEXT_CHARS  = 1000000;
XL_VECTOR_MAX_IMAGE_SIDE        = 8192;
XL_VECTOR_MAX_IMAGE_PIXELS      = 32 * 1024 * 1024;
XL_VECTOR_MAX_IMAGE_BYTES       = 64 * 1024 * 1024;
XL_VECTOR_MAX_COORD             = 1000000000;

이중 두 개는 주석이 필요합니다. 디바이스 컨텍스트 깊이 상한 32는 SaveDCRestoreDC 레코드가 중첩되고 불균형한 스트림은 영원히 푸시할 수 있기 때문에 존재합니다. 32는 실제 메타파일에는 넉넉하고 강제 비용도 쌉니다. 좌표 상한은 좌표가 변환에 들어가기 때문에 존재합니다. 정수 범위 한계 근처의 값은 무한이거나 넘치는 변환 결과를 만들고, 그 뒤로 하류의 모든 바운딩 박스 계산은 엉터리가 됩니다. 좌표를 파스 시점에 클램프하는 것이 지오메트리의 모든 소비자를 방어하는 것보다 훨씬 추론하기 쉽습니다

HotXLS lxVectorScene의 디코딩 예산 상수: 레코드, 핸들, DC 깊이, 명령, 점, 텍스트, 이미지 크기, 좌표 클램프
모든 한계는 파싱 중 강제되는 이름 붙은 상수이며, DC 깊이 상한과 좌표 클램프가 가장 주목할 만합니다

장면 사용하기

디코더는 여러분이 소유할 오브젝트와 명령 개수, 명목 크기, 표현하지 않기로 한 드로잉 레코드의 개수를 돌려줍니다

uses
  lxVectorScene;

var
  Scene: TXLSVectorScene;
  Error: WideString;
  I: Integer;
begin
  // Data는 통합 문서에서 가져온 원본 그림 페이로드를 담습니다
  if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
  begin
    // 거부됨: 헤더, 상한, 총수, EOF 위치 또는 예산
    LogReject('metafile rejected: ' + Error);
    Exit;
  end;
  try
    if Scene.SkippedDrawRecords > 0 then
      LogWarning(Format('%d drawing records outside the safe subset',
        [Scene.SkippedDrawRecords]));
    for I := 0 to Scene.Count - 1 do
      case Scene.Commands[I].Kind of
        xlsvcRectangle: DrawRect(Scene.Commands[I]);
        xlsvcEllipse:   DrawEllipse(Scene.Commands[I]);
        xlsvcPolyline,
        xlsvcPolygon,
        xlsvcBezier:    DrawPath(Scene.Commands[I]);
        xlsvcText:      DrawText(Scene.Commands[I]);
        xlsvcImage:     DrawImage(Scene.Commands[I]);
      end;
  finally
    Scene.Free;
  end;
end;

명령 레코드는 백엔드가 필요한 모든 것을 실되 디바이스를 요구하는 것은 아무것도 실지 않습니다. 펜 존재, 색, 폭, 스타일, 브러시 존재와 색, 지오메트리, 그리고 텍스트라면 문자열, 폰트 이름, 크기, 스타일, 정렬입니다. 이것이 같은 장면을 화면 캔버스 렌더러와 SVG 라이터 양쪽이 쓸 수 있게 하고, 벡터 경로가 미리보기와 내보내기 사이에서 갈라지지 않는 이유입니다. 워크시트 콘텐츠의 화면 렌더링 일반은 커스텀 VCL 그리드 렌더링 문서에서 다룹니다

그림을 거부해도 통합 문서는 상하지 않습니다

이 설계의 중요한 성질은 거부된 디코딩이 렌더링에만 영향을 준다는 것입니다. 원본 페이로드는 모델에 남으므로, 열고 다시 저장하는 통합 문서는 안전한 디코더가 그릴 수 있었는지와 무관하게 메타파일 그림을 바이트 단위로 실어 나갑니다. 기존의 상한 있는 래스터 경로도 폴백으로 계속 쓸 수 있습니다. 다시 말해 엄격한 파서는 실행되는 것을 게이트하지 보존되는 것을 게이트하지 않으며, 이 구분이 보안 동기의 변경이 데이터 손실 변경으로 변하지 않고 배포되게 합니다

왕복에서 훼손 없이 살아남는 오브젝트 모델의 부분들을 포함한 드로잉 오브젝트 처리 일반은 차트, 이미지, 드로잉 문서에서 다룹니다

서버 배포에 이것이 남기는 위치

서비스에서 사용자가 올린 통합 문서를 렌더링한다면 실무적 입장은 이제 방어 가능합니다. 메타파일 그림은 감사할 수 있는 코드가 해석하고, 읽을 수 있는 상수가 상한을 두며, 그래픽스 드라이버에 절대 넘겨지지 않습니다. 정직한 단서는 커버리지입니다. 드로잉 도구가 만든 복잡한 메타파일은 건너뛴 레코드 카운터에 걸릴 것이고, 그에 대한 답은 화이트리스트를 조용히 넓히는 것이 아니라 카운터를 표면화하는 것입니다. 부분적으로 그리면서 그렇다고 말하는 그림은 지원 대화이고, 잘못 그리면서 아무 말도 하지 않는 그림은 고객의 버그 보고입니다

HotXLS는 Excel 설치 없이 Delphi와 C++Builder에서 XLS, XLSX, ODS, CSV를 네이티브로 처리하며, 같은 상한 파스 철학이 컨테이너, 수식, 드로잉 계층을 관통합니다. 포맷과 보안 세부는 HotXLS Delphi spreadsheet component 제품 페이지에 정리되어 있습니다