기술 문서

HotXLS 위험 수식 콜백: CALL과 WEBSERVICE 차단

HotXLS는 CALL, REGISTER.ID, WEBSERVICE, DDE를 포함한 20개의 위험한 수식 이름을 옵트인하지 않는 한 여러분의 Delphi 사용자 함수 콜백으로 라우팅하기를 거부합니다. 통합 문서 속성 AllowUnsafeFormulaCallbacks의 기본값은 False이고, 검사는 인자가 하나 평가되기 전에 돌며, 거부된 호출은 핸들러 하나도 실행하지 않고 xlfeUnsafeFunctionDenied를 보고합니다

이것이 필요해진 시나리오는 지극히 평범합니다. 서비스가 업로드된 XLS나 XLSX 파일을 받아 서버 쪽에서 재계산하고 몇 개 합계를 읽어 돌려줍니다. 호스트 애플리케이션은 몇 년 전 비즈니스 함수 두어 개를 위해 OnUserFunction 핸들러를 등록했고, 어느 시점부터 그 핸들러는 인식 못 하는 것은 전부 플러그인 테이블로 넘기는 catch-all 분기를 키웠습니다. 팀원 누구도 셀에 =WEBSERVICE(...)를 입력한 적 없습니다. 업로더가 입력했습니다. 열기, 재계산, 저장을 거치는 동안 그 수식을 온전히 보존하는 것은 파일 충실도 기능입니다. 소켓이나 파일을 열 수 있는 호스트 코드에 그것이 닿게 두는 것은 인가 결정이고, HotXLS가 둘을 분리하기 전까지는 라이브러리가 여러분 대신 그 결정을 조용히 내리고 있었습니다

수식을 보존하는 것이 실행 허가로 변한 이유는?

근본 원인은 단일 폴백 경로였습니다. HotXLS는 아는 Excel 함수 이름은 전부 파싱하지만, 아는 이름 전부가 계산 엔진에 구현을 갖지는 않습니다. 인식은 되는데 구현이 없는 빌트인은 진짜 커스텀 이름과 같은 사용자 정의 함수 폴백으로 떨어졌고, 그래서 CALL과 REGISTER.ID는 여러분의 DISCOUNT나 REGIONRATE와 디스패치 경로를 공유했습니다. WEBSERVICE나 DDE 같은 모르는 이름도 마찬가지로 통합 문서 레지스트리, 프로세스 전역 레지스트리, 이벤트 핸들러의 같은 이름 항목에 부합할 수 있었습니다. 그 폴백의 작동 방식은 HotXLS가 OnUserFunction으로 커스텀 함수를 해석하는 방법에서 다룹니다. 문제는 그 경로 어디에도 그 이름 자체가 정상적인 호스트가 실행해야 하는 것인지 묻는 존재가 없었다는 점입니다

여기서 "모르는"이 무슨 뜻인지는 디스패치 순서가 좌우합니다. 엔진이 네이티브로 평가할 수 없는 호출은 순서대로 어휘 LAMBDA와 LET 바인딩 — HotXLS 수식 엔진의 클로저 지원이 가장 먼저 해석 — 에, 이어서 RegisterUserFunction으로 등록된 통합 문서 로컬 함수에, 다음으로 TXLSWorkbook.RegisterGlobalUserFunction의 프로세스 전역 함수에, 마지막으로 OnUserFunction과 OnUserFunctionEx 이벤트에 넘겨집니다. 전부 사양할 때에야 진짜 모르는 함수가 #NAME?이 됩니다. lambda 조회 다음의 모든 단계는 여러분이 쓴 코드에 제어를 넘기는데, 안전 검사가 임의 핸들러 안이 아니라 체인 전체 앞에 앉아야 하는 이유가 바로 이것입니다

HotXLS가 위험 수식 콜백을 게이트하는 방식: GetValueItemUserFunction은 인자 배열이 만들어지기도 전에, 어떤 resolver가 돌기 전에 이름을 검사하므로, 중첩된 =WEBSERVICE(AUDIT_TOKEN())은 lxErrorUnsafeFunctionDenied로 빠져나가 감사 로그는 빈 채로 유지되고, 안전한 호출은 LAMBDA와 LET 바인딩부터 OnUserFunction까지 체인을 걸어갑니다
모든 단계가 사양할 때에야 진짜 모르는 함수가 #NAME?이 되므로, 안전 검사는 임의 핸들러 안이 아니라 체인 전체 앞에 앉습니다

HotXLS는 기본적으로 어떤 함수 이름을 차단할까요?

lxCalc.pas의 XLSFormulaCallbackIsUnsafe는 20개 이름의 고정 거부 집합을 들고 있습니다. DDE, CALL, REGISTER, REGISTER.ID, WEBSERVICE, RTD, SQL.REQUEST, EXEC, RUN, CREATE.OBJECT, APP.ACTIVATE, SEND.KEYS, OPEN, SAVE, SAVE.AS, FOPEN, FWRITE, FWRITELN, FCLOSE, FILE.DELETE입니다. Excel이나 그 매크로 언어에서 네이티브 코드를 로드하거나 네트워크에 닿거나 다른 프로세스와 통신하거나 파일 시스템을 건드리는 이름들입니다. 비교 전에 함수는 주변 공백을 잘라내고 이름을 대문자로 바꾸며 하나의 _XLFN. 또는 _XLWS. 접두사를 떼어내므로, 더 최신 Excel 빌드가 쓴 _xlfn.webservice도 맨 이름과 똑같이 잡힙니다. 목록은 Classic, XLSX, ODS 파서가 아니라 계산기 경계에 살고, 그래서 하나의 AST, 하나의 BIFF 토큰 스트림, 하나의 변환된 통합 문서가 동일하게 동작합니다

HotXLS가 unsafe-callback 비교 전에 함수 이름을 정규화하는 방식: 공백을 잘라내고 이름을 대문자로 바꾸고 하나의 _XLFN. 또는 _XLWS. 접두사를 떼어내서 _xlfn.webservice도 맨 이름처럼 잡히게 한 뒤, 결과를 lxCalc.pas의 20개 이름 고정 거부 집합과 정확히 대조합니다
목록은 네이티브 코드를 로드하고 네트워크에 닿고 다른 프로세스와 통신하고 파일 시스템을 건드리는 이름들을 아우릅니다. DDE, CALL, WEBSERVICE부터 FWRITE와 FILE.DELETE까지입니다

이것에 의지하기 전에 알아 둘 가장자리가 두 개 있습니다. 매칭은 정확 일치라서 MYWEBSERVICE로 등록한 핸들러는 영향받지 않고, 반대로 우연히도 OPEN이나 RUN이라 불리는 정당한 사내 UDF는 이제 기본적으로 거부됩니다. 거부 집합은 여러분 핸들러를 위한 샌드박스도 아닙니다. catch-all 분기가 임의의 플러그인 이름을 실행한다면, 게이트는 유명한 위험 이름만 막을 뿐 그 외에는 아무것도 못 합니다. 근본적인 해결은 여전히 명시적 allowlist를 SameText로 대조하고 자기 소유가 아닌 것에는 Handled를 False로 남겨 두는 핸들러입니다

게이트는 왜 인자 평가 전에 돌아야 할까요?

인자가 계산된 뒤에 발화하는 게이트는 너무 늦습니다. 인자 자체가 여러분의 코드를 호출할 수 있기 때문입니다. GetValueItemUserFunction은 이름을 먼저 검사하고, 인자 배열을 만들기 전에, resolver나 어느 레지스트리에 물어보기 전에, 심지어 핸들러가 아예 할당되지 않았다는 것을 눈치채기 전에 lxErrorUnsafeFunctionDenied로 빠져나갑니다. 그 순서가 아래 중첩 사례를 무력화하는 것입니다. 바깥 호출은 어차피 거부되지만, 그렇지 않으면 무해해 보이는 안쪽 UDF가 먼저 발화해 자기 부작용을 남겼을 것입니다

procedure TImportService.HandleUdf(Sender: TObject;
  const FunctionName: WideString; const Args: Variant;
  var Value: Variant; var Handled: Boolean);
begin
  if SameText(FunctionName, 'AUDIT_TOKEN') then
  begin
    FAuditLog.Add('AUDIT_TOKEN evaluated');   // 호스트 코드의 부작용
    Value := 'token-42';
    Handled := True;
  end;
end;

Book.OnUserFunction := HandleUdf;
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
// Eval.Issue.NativeCode = -106, 그리고 FAuditLog는 여전히 비어 있음

통합 문서 기본값과 호출별 TXLSFormulaEvaluationOptions

통합 문서 플래그가 기본값이고 호출별 옵션이 최종 결정입니다. TXLSWorkbook.AllowUnsafeFormulaCallbacks와 TXLSXWorkbook.AllowUnsafeFormulaCallbacks는 일반 재계산, Calculate, 두 인자짜리 EvaluateFormulaAt, 평가 템플릿, 읽기 전용 뷰, 그리고 XLSX에서는 병렬 재계산 풀의 모든 워커를 지배합니다. 명시적인 TXLSFormulaEvaluationOptions 레코드를 받는 진입점은 그 호출의 판정으로 Options.AllowUnsafeFormulaCallbacks를 받아들이며 통합 문서 속성과 OR 하지 않습니다. 이 비대칭은 의도적입니다. 신뢰하는 내부 작업은 통합 문서 전체를 뒤집지 않고 RTD 조회 하나를 허가할 수 있고, 전역으로 옵트인된 통합 문서도 민감한 평가를 거부로 강제할 수 있습니다

var
  Options: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
begin
  // 통합 문서는 잠긴 채 유지, 신뢰하는 호출 하나만 통과
  Book.AllowUnsafeFormulaCallbacks := False;
  Options := XLSDefaultFormulaEvaluationOptions;
  Options.AllowUnsafeFormulaCallbacks := True;
  Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);

  // 통합 문서는 옵트인, 그러나 업로드 텍스트의 이 평가는 그렇지 않음
  Book.AllowUnsafeFormulaCallbacks := True;
  Options := XLSDefaultFormulaEvaluationOptions;   // 플래그는 다시 False
  Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
  if Eval.Status = xlfeUnsafeFunctionDenied then
    LogRejected(Eval.Issue.Message);
end;

통합 문서 속성을 토글하면 두 엔진 모두에서 의존성 그래프도 dirty로 표시됩니다. 이 단계가 없으면 콜백이 허용되던 때에 계산된 캐시 결과가 허용이 철회된 뒤에도 나올 수 있고, 캐시된 xlfeUnsafeFunctionDenied 결과가 옵트인보다 오래살아남을 수 있습니다. 새 상태는 xlfeFailed 뒤에 TXLSFormulaEvaluationStatus에 붙었으므로 서수 10이고 기존 서수는 전부 값을 유지합니다. 같은 꼬리-추가 규칙이 옵션 레코드 필드와 IXLSWorkbook 게터와 세터에도 적용되지만, 이전 릴리스 기준으로 빌드된 소비자는 여전히 재컴파일이 필요합니다

저장 시 모르는 수식과 위험 수식 텍스트는 어떻게 될까요?

수식을 보존하는 것과 실행하는 것은 이제 별개의 질문이고, 진입 정책은 첫 번째 질문에만 답합니다. 어느 통합 문서 클래스의 FormulaEntryPolicy도 UnknownFunctionMode와 UnknownNameMode를 갖는데 둘 다 기본값은 xlfusmReject입니다. 그래서 모르는 호출을 담은 수식을 평범한 Formula 속성으로 대입하면 셀 값, 수식 캐시, 의존성이 바뀌기 전에 거부됩니다. ValidateFormulaEntry는 같은 결정을 부작용 없이 보고합니다. 파일 로드, 복사, 형식 변환 같은 신뢰 경로는 그 사용자 진입 정책을 우회합니다. 엄격한 기본값이 여러분이 그저 열고 있을 뿐인 파일에 이미 존재하는 심볼을 거부해서는 안 되기 때문입니다

var
  Policy: TXLSFormulaEntryPolicy;
begin
  Policy := Book.FormulaEntryPolicy;
  Policy.UnknownFunctionMode := xlfusmPreserve;   // 호환 진입
  Book.FormulaEntryPolicy := Policy;
  Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)';  // 저장은 되지만 승인된 것은 아님
  Book.SaveAs('rates.xls');
end;

클래식 BIFF8에서 모르는 호출에는 자기 토큰이 없으므로, HotXLS는 Excel이 애드인 함수를 쓰는 방식으로 그것을 씁니다. 수식은 XTI 항목이 두 시트 인덱스를 $FFFE로 설정한 채 애드인 SUPBOOK을 가리키는 PtgNameX 토큰($59)을 받고, 그 뒤에 인자 토큰과 함수 번호 255에 이름 슬롯을 포함한 인자 개수를 지닌 PtgFuncVar가 따라옵니다. 그 기반이 되는 ExternName 본문은 영 6바이트, 길이 바이트와 Unicode 플래그, UTF-16 함수 이름, 그리고 #REF!를 담은 PtgErr, 즉 $1C $17의 2바이트 수식입니다. 라이터는 255자를 넘는 이름, 29개를 넘는 인자, BIFF5 대상을 거부합니다. HotXLS가 이런 애드인 SUPBOOK 항목을 외부 통합 문서 링크와 나란히 어떻게 분류하는지는 BIFF 외부 링크의 SUPBOOK과 XTI 분류 규칙에 설명되어 있습니다. XLSX는 날 함수 텍스트를 유지하고 ODS는 자기 msoxl: 수식을 유지하며, 어떤 형식에서든 =WEBSERVICE(...)를 저장한 파일은 텍스트가 온전한 채로 다시 열리고 기본값으로 여전히 xlfeUnsafeFunctionDenied로 평가됩니다

HotXLS가 모르는 수식 호출을 클래식 BIFF8에 쓰는 방식: 수식은 XTI 항목이 두 시트 인덱스 $FFFE로 애드인 SUPBOOK을 가리키는 PtgNameX 토큰을 지니고, 그 뒤에 인자 토큰과 함수 번호 255의 PtgFuncVar가 따라오며, 그 기반인 ExternName 본문은 #REF!를 담은 2바이트 $1C $17 PtgErr로 끝납니다
XLSX는 날 함수 텍스트를 유지하고 ODS는 자기 msoxl: 수식을 유지하므로, =WEBSERVICE(...)를 저장한 파일은 텍스트가 온전한 채로 다시 열리고 기본값으로 여전히 xlfeUnsafeFunctionDenied로 평가됩니다

파이프라인이 자기가 작성하지 않은 통합 문서를 평가한다면 AllowUnsafeFormulaCallbacks를 False에 두고, 핸들러는 명시적 allowlist로 유지하고, 호출별 옵션은 수식 출처가 여러분 것인 곳에만 허가하세요. 전체 콜백, 진입 정책, 평가 API는 HotXLS Delphi 스프레드시트 컴포넌트와 함께 문서화되어 있습니다