Skip to content

fix(security): guard ImageSharp decoding of user-uploaded images - #103

Merged
cct08311github merged 2 commits into
dotnet10from
fix/78-imagesharp-upload-mitigation
Oct 19, 2026
Merged

cct08311github merged 2 commits into
dotnet10from
fix/78-imagesharp-upload-mitigation

Conversation

@cct08311github

@cct08311github cct08311github commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

背景

SixLabors.ImageSharp 3.1.12 有 5 個 advisory(3 個 High),僅在 4.x(major)修復。它是經由 DUWENINK.Captcha 0.10.2 間接引入,而該套件尚無支援 ImageSharp 4 的版本,因此現階段無法升級。維護者決定:先做緩解,等上游支援後再升級。

變更

針對所有解碼「使用者上傳影像」的路徑,在完整解碼前先以標頭資訊(Image.Identify)驗證:

  • 格式白名單:JPEG、PNG、GIF、BMP、WebP(TIFF 與其他格式一律拒絕)
  • 僅註冊這五種格式的專用 Configuration,同時用於 Identify 與 Load(TIFF/BigTIFF 等根本不會被解析,避免 GHSA-wmxv-xphr-5c9g 在白名單前觸發)
  • 檔案大小上限(預設 10 MB)
  • 像素尺寸上限(預設 8192x8192,且總像素 <= 25 MP)
  • 只載入第一幀(MaxFrames=1,縮圖輸出本來就是 JPEG 首幀,行為不變)
  • 縮圖目標尺寸上限(預設 4096x4096,長整數運算,最小 1);超限時 UploadImage 回既有 BadRequest,GetFile 回原始檔
  • 上限可由設定區段 ImageUploadLimits(MaxBytes / MaxWidth / MaxHeight / MaxPixels / MaxFrames / MaxResizeWidth / MaxResizeHeight)調整

已防護的解碼路徑(全 repo grep 後僅此兩處,皆在 demo):

  1. FileApiController.UploadImage(縮圖上傳):驗證失敗回傳既有的 BadRequest(Sys.UploadFailed)
  2. FileApiController.GetFile(縮圖輸出):驗證失敗時維持既有行為,回傳原始檔案(不可 seek 的 provider stream 改由已緩衝的副本回傳);兩處皆會 Dispose 影像並以 ILogger 記錄拒絕原因

新增 demo/.../Areas/_Admin/ImageDecodeGuard.cs(僅 demo,src/ 沒有解碼使用者影像的程式碼,故未動 src、無公開 API 變更)。Captcha 產生程式碼完全未動(只編碼自行繪製的影像)。

測試結果

新增 ImageDecodeGuardTest(沿用 Core.Test 既有的 MSTest 慣例),涵蓋:PNG/JPEG/GIF/BMP/WebP 通過、TIFF 拒絕、寬度超限與總像素超限拒絕、位元組超限拒絕(含不可 seek 的 stream)、非影像與空內容拒絕。

  • 新增迴歸測試:24 byte 惡意 BigTIFF 標頭須在 2 秒內被拒絕([Timeout])、動態 GIF 只載入首幀、尺寸夾限、不可 seek 緩衝回傳
  • dotnet build WalkingTec.Mvvm.sln:0 錯誤
  • dotnet test:Core.Test 368 通過 / 0 失敗;Admin.Test 9 通過 / 0 失敗

對 LayUI/下游的影響

應為無:未變更任何公開 API、Captcha 行為與 LayUI 登入頁;僅在 demo 的影像上傳/縮圖路徑增加驗證,合法的常見格式與一般尺寸行為不變。

後續(未在本 PR 處理的低風險項目)

  • 對帶 width/height 的 GetFile,非圖片或大檔仍會先讀取至多 MaxBytes 才拒絕(可依副檔名先過濾)
  • 超過 MaxBytes 的合法大圖,縮圖請求改回傳原始檔(行為變更,已知)
  • 對併發解碼加 SemaphoreSlim 上限
  • 日後若讀取 ICC profile Entries 需注意 GHSA-gwg2-r3hj-4w44(已在程式碼註記)

Refs #78

🤖 Generated with Claude Code

SixLabors.ImageSharp 3.1.12 has 5 advisories (3 High) that are only fixed
in 4.x (major). It is pulled in transitively via DUWENINK.Captcha 0.10.2,
which has no ImageSharp 4 compatible release, so upgrading is blocked.

As a short-term mitigation, every path that decodes user-supplied images
(FileApiController.UploadImage / GetFile thumbnailing) now validates the
input from the header before any full decode: format allowlist (JPEG, PNG,
GIF, BMP, WebP; TIFF and others rejected), max bytes (default 10 MB) and
max dimensions (8192x8192, 40 MP). Limits are configurable via the
"ImageUploadLimits" section. Rejections reuse each endpoint's existing
error behaviour. Captcha generation is untouched (it only encodes images
it draws itself) and no public API changes.

Refs #78

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cct08311github

Copy link
Copy Markdown
Owner Author

Security review: PR #103(ImageSharp 3.1.12 緩解)

Verdict:REQUEST CHANGES(方向正確、能擋掉大多數向量,但有一個已實測的繞過:guard 以 Image.Identify 偵測格式,TIFF 在白名單檢查之前就已被解析。)

1. 各 advisory 對 3.1.12 的影響與本 PR 是否擋得住

套用 3.1.12 的共 5 個(EXR 與 ICC LUT16 僅影響 >=4.0.0,不適用):

Advisory 嚴重度 路徑 本 PR 後的殘餘風險
GHSA-jjfr-hcj7-qf5w TIFF CCITT T6 encoder High 編碼 1-bit CCITT TIFF 本 app 只 SaveAsJpeg,不編碼 TIFF;不可達。殘餘:無
GHSA-j9gm-c75j-xc9q TIFF CCITT T4 encoder High 同上 同上,不可達。殘餘:無
GHSA-j3p4-wp97-rph4 HistogramEqualization High 需 float TIFF + 該 processor 程式只呼叫 Resize,不可達(TIFF 亦被擋)。殘餘:無
GHSA-wmxv-xphr-5c9g BigTIFF IFD 無進度迴圈 Medium TIFF 解碼/Identify 未被擋住(見 H1)。我以 3.1.12 實測:24 byte BigTIFF 對 Image.Identify 即卡住超過 8 秒(未進到白名單判斷)。這是匿名可達的 CPU thread DoS(GetFile 為 [Public],只要檔案已存在於 provider;UploadImage 則需上傳權限)。
GHSA-gwg2-r3hj-4w44 ICC CLUT 配置 Medium v1-v3 僅在存取 IccProfile.Entries 時觸發(JPEG/PNG/WebP 內嵌 ICC 都可帶) 程式不存取 Entries,3.1.12 不會自動轉換,不可達。允許的解碼器 (JPEG/PNG/WebP) 可帶 ICC,所以殘餘風險「僅在日後有人讀 ICC metadata 時」存在;建議在 code 註記。

先前已修(3.1.11/3.1.7/3.1.5 等的 GIF 無限迴圈、GIF 過量配置、OOB write)在 3.1.12 已含,非本 PR 範疇;但 GIF 仍是「允許」格式,見 M2。

2. 發現(依嚴重度)

HIGH

  • H1 TIFF(及其他非白名單格式)在白名單檢查前就被解析 — demo/.../Areas/_Admin/ImageDecodeGuard.cs,Validate() 內 Image.Identify(seekable)。預設 Configuration 註冊所有格式(含 TIFF/TGA/PBM/QOI),Identify 會對 TIFF 走完整 IFD 解析,因此 BigTIFF 迴圈在 IsAllowed 之前就觸發;PR 描述「TIFF 一律拒絕」對 BigTIFF DoS 不成立,單元測試也只測了合法 TIFF。已實測修法:使用僅註冊五種格式的 Configuration,BigTIFF PoC 立即得到 UnknownImageFormatException:
    static readonly Configuration Cfg = new Configuration(new JpegConfigurationModule(), new PngConfigurationModule(),
        new GifConfigurationModule(), new BmpConfigurationModule(), new WebpConfigurationModule());
    var opt = new DecoderOptions { Configuration = Cfg, MaxFrames = 1 /* 見 M2 */ };
    Image.Identify(opt, ms); ... Image.Load(opt, ms);
    (並可再加一層 magic-bytes 預檢作 defense in depth。)請補一個 BigTIFF 24 byte 迴歸測試(含 timeout)。

MEDIUM

  • M1 縮放目標尺寸無上限 — FileApiController.cs UploadImage(約 L56-68)與 GetFile(約 L111-125):width/height 來自 query,Resize(width, height) 無上限;GetFile 為 [Public],?width=30000&height=30000 即配置數 GB。此為既有問題但與本 PR 的「解碼前限制」目的同一類 DoS,建議套用 limits.MaxWidth/MaxHeight 並檢查 <=0。另 height * oimage.Width / oimage.Height 可得 0(Resize 擲例外;UploadImage 無 try/catch 會 500)與 int overflow。
  • M2 GIF 幀數未限制 — Guard 只檢查單幀尺寸;動態 GIF 的記憶體/CPU = 幀數 x 像素。Image.Load 預設載入全部幀,且 8192x8192 的上限偏寬鬆(單幀 Rgba32 約 256MB,MaxPixels 40MP 約 160MB),並行請求易 OOM。建議 DecoderOptions.MaxFrames = 1(縮圖只需首幀)、降低預設 MaxPixels,或對併發解碼加 SemaphoreSlim。
  • M3 GetFile 失敗後 fallback 讀到已消耗的 stream — FileApiController.cs L105-111:只有 CanSeek 時才 Position = 0;不可 seek 的 provider(非本機檔案)在 guard 拒絕或失敗後會回傳空 body/截斷內容,同時 TryLoad 已經把整個串流讀入記憶體。建議:不可 seek 時先自行 buffer 並以 buffer 回應,或 guard 拒絕時重新取檔。

LOW

  • L1 GetFile 的 catch 吞例外且未釋放資源(既有但本 PR 觸及):Mutate/SaveAsJpeg 例外時 oimage 不會 Dispose,且 Response.Body 可能已部分寫出再接 fallback。建議 using var oimage。
  • L2 對任何帶 width/height 的 GetFile 請求(含 >10MB 的非圖片/mp4)都會先讀取最多 MaxBytes 才拒絕:TryBuffer 對不可 seek 的來源無法預先拒絕;匿名者可反覆觸發 I/O。可先看副檔名/Content-Type,僅圖片副檔名才進 guard。
  • L3 > MaxBytes 的合法大圖縮圖會改回傳原始檔(行為變更),建議在 PR 說明註記。
  • L4 UploadImage 的 using upstream 與 oimage 在 Resize 例外時未 Dispose;error 參數被所有呼叫端丟棄(無記錄),建議 log 拒絕原因(不含檔案內容)以利偵測攻擊。
  • L5 僅以 Identify 的標頭尺寸作限制:解碼器各自重讀標頭,目前同一 buffer 所以無 TOCTOU;PNG/WebP 的實際記憶體配置仍以標頭為準,OK。

3. 其他解碼路徑與 Captcha

  • 全 repo(含 src/、demo/)grep:使用者輸入的 ImageSharp 解碼僅有 FileApiController.UploadImage 與 GetFile 兩處,PR 皆已包覆。Upload(非縮圖)直接存原檔,不解碼;任意檔案仍可被存入,之後只有帶 width/height 才進 guard,行為正確。
  • DUWENINK.Captcha 0.10.2(依賴 ImageSharp 3.1.12、Drawing 2.1.7):檢視套件原始碼,只用 Image<Rgba32> 繪製驗證碼並編碼輸出,唯一的 Load 為內嵌字型資源(GetManifestResourceStream);本 repo 只在 FrameworkServiceExtension.cs:522 註冊 DI,沒有任何 controller 餵入攻擊者影像。判定不可由攻擊者資料觸發。

4. 建議合併條件

  1. 必要:H1(受限 Configuration + BigTIFF 迴歸測試)。
  2. 強烈建議:M1(縮放尺寸上限)、M2(MaxFrames=1)、M3(不可 seek fallback)。
  3. 其餘 LOW 可後續 Issue 追蹤。
  4. Issue deps: ImageSharp vulnerabilities via captcha dependency #78 不應在合併後直接關閉,應保留「待 DUWENINK.Captcha 支援 ImageSharp 4.1.2+ 後升級」的追蹤。

測試環境:.NET 10.0.401 / ImageSharp 3.1.12 實測 BigTIFF Identify(未受限 Configuration:卡住;受限 Configuration:UnknownImageFormatException)。未修改任何檔案。

Review of #103 found that Image.Identify with the default Configuration
parses TIFF before the allowlist runs, so a 24-byte BigTIFF
(GHSA-wmxv-xphr-5c9g) still hangs the request. The guard now uses a
dedicated Configuration that registers only JPEG/PNG/GIF/BMP/WebP for
both Identify and Load, so other formats are never parsed.

Also: clamp requested thumbnail size (default max 4096) and use long
math for the aspect ratio; load only the first frame (MaxFrames=1) and
lower default MaxPixels to 25 MP; serve the GetFile fallback from the
buffered copy when the provider stream is not seekable; dispose images
on all paths and log rejection reasons.

Refs #78

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cct08311github

Copy link
Copy Markdown
Owner Author

已依 review 修正(commit 見分支最新):

  • H1:改用僅註冊 JPEG/PNG/GIF/BMP/WebP 的專用 Configuration(Identify 與 Load 皆用),新增 24 byte BigTIFF 迴歸測試(Timeout 2s)
  • M1:縮圖目標尺寸夾限(預設 4096,可設定)、長整數運算、最小 1;超限 UploadImage 回既有 BadRequest,GetFile 回原始檔
  • M2:MaxFrames=1(只載首幀,與原本 JPEG 輸出一致)、MaxPixels 預設降為 25 MP
  • M3:不可 seek 時由已緩衝副本回傳 fallback
  • LOW:L1/L4 影像於所有路徑 Dispose、拒絕原因以 ILogger 記錄;L2/L3/併發/ICC 列於 PR 描述「後續」
    build 0 錯誤;Core.Test 368 通過、Admin.Test 9 通過。

@cct08311github
cct08311github merged commit 1680270 into dotnet10 Oct 19, 2026
5 checks passed
@cct08311github
cct08311github deleted the fix/78-imagesharp-upload-mitigation branch October 19, 2026 13:07
cct08311github added a commit that referenced this pull request Oct 19, 2026
…rd (#151)

The OpenApi 2.12.2 / Swashbuckle 10.3.0 upgrade (#54) changes generated
swagger.json for untyped properties (nullable dropped); client generators
should know it is semantically equivalent. The ImageSharp mitigation (#103)
adds a configurable ImageUploadLimits section and changes rejection
behaviour, which belongs in the unreleased notes.

Closes #150

Co-authored-by: cct0831 <cct0831github@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

security Security fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant