本卡屬 FR-195(Agent 資安修正),第 2 批 F7。修 FR-186 掃描的 S6 F1(3/3 票,FR-177 #226 修法的旁路)、C3 F1、C6 F1(皆 3/3 票),嚴重度:中。
三個地方會把對方給的資料整份讀進記憶體、讀完才檢查大小,對方故意給很大的東西,Agent 整個行程就被系統砍掉(心跳、掃描、主控台全停)。①tar 壓縮包的長檔名/PAX 擴充標頭宣告多大,Python 的 tarfile 就一次讀多大——FR-177 擋了「很多小檔」,沒擋「一個標頭宣告超大」。②nmap 在客戶執行主機上產生的報告,Agent 用 SFTP 整份讀回來才比對 60 MB 上限。③SSH 遠端指令的輸出整段累積在記憶體,沒有上限。
b8b1158 重核過,仍然有效)guidant_agent/shared/archive_safety.py:190 tarfile.open(archive_path, "r:*");:224-238 _count_tar_members 用 for member in archive 逐成員讀標頭——tarfile 讀 L/K/x/g 型擴充標頭時會依標頭宣告的長度一次 read,額度檢查還沒機會跑。guidant_agent/scan/adapters/connectors/nmap.py:511 已經 report_stat = sftp.stat(remote_report)、:523 檢查 0 bytes,但沒檢查上限;:528-529 with sftp.open(...) as f: xml_bytes = f.read() 整份讀。上限常數在 :129 _XML_MAX_BYTES = 60 * 1024 * 1024,現在是讀完之後才用。guidant_agent/scan/adapters/connectors/base.py:171-175 _drain 把 channel.recv(4096) 一直寫進 io.BytesIO,沒有上限;呼叫端在 :115、:125。guidant_agent/shared/archive_safety.py
guidant_agent/scan/adapters/connectors/nmap.py
guidant_agent/scan/adapters/connectors/base.py
test/test_fr177_resource_limits.py
test/test_nmap_connector.py
archive_safety.py:在同檔加一個小類別 _CappedReader(包住一個檔案物件,read(n) 累計讀到的位元組數,超過上限就 raise RuntimeError,訊息沿用 check_archive_quota 的句型、用 labels.subject)。tar 分支改成自己開解壓串流再交給 tarfile:依副檔名/檔頭選 gzip.open、bz2.open、lzma.open 或直接 open(..., "rb")(判斷壓縮方式用檔頭魔術位元組:\x1f\x8b=gzip、BZh=bz2、\xfd7zXZ=xz,其餘當未壓縮),外面包 _CappedReader(stream, max_bytes + max_files * 1024),再 tarfile.open(fileobj=capped, mode="r|")。🔴 mode="r|" 是串流模式,不能回頭 seek:現有流程是先 _count_tar_members 走一遍再 _extract_members 解壓,串流模式下第二遍會失敗。做法:數檔案那一遍用 _CappedReader+r| 開一次、走完關掉;解壓那一遍照原本 tarfile.open(archive_path, "r:*") 再開一次(第一遍已證明整包解開不超過上限,第二遍安全)。_count_tar_members 的回傳改成不回成員清單,解壓端自己 for member in archive;_extract_members 的呼叫端跟著調整。_count_tar_members 迴圈裡,member.type 是 tarfile.GNUTYPE_LONGNAME、GNUTYPE_LONGLINK、XHDTYPE、XGLTYPE 這四種的話其實 tarfile 已經在 next() 內部處理掉、迭代拿不到;所以靠 _CappedReader 的總量上限擋就夠,不用另外判斷型別。docstring 寫清楚這一點(為什麼不能只靠數成員)。nmap.py:在 :523 的 0 bytes 檢查後面加一段——if report_stat.st_size > _XML_MAX_BYTES: raise RuntimeError(...)(訊息講報告大小與上限);:529 改成 f.read(_XML_MAX_BYTES + 1),讀回來長度超過上限一樣 raise(對方可以在 stat 之後把檔案變大)。🔴 _scan_one_host 在函式行數白名單上(95 行):不能變長。把「stat→檢查→開檔→讀」這段抽成同檔的一支小函式 _read_remote_report(sftp, path, host) -> bytes,_scan_one_host 裡換成一行呼叫,行數會變少;改完把 test/test_file_and_function_size.py 裡 NmapConnector._scan_one_host 的數字改成新的行數(只准改小)。base.py _drain:加一個模組常數 _REMOTE_OUTPUT_MAX = 8 * 1024 * 1024,out/err 各自累計超過就停止寫入但繼續把通道讀空(不讀空的話遠端會卡在寫),並在該緩衝尾端加一行「…(輸出超過 8 MB,其餘略)」。做法:寫入前 if out.tell() < _REMOTE_OUTPUT_MAX: out.write(...)。不要 raise——輸出太長不該讓掃描失敗,只是不留全部。正常使用不受影響。超過上限的壓縮包、nmap 報告會得到明確的錯誤訊息;SSH 指令輸出超過 8 MB 的部分不保留。
test/test_fr177_resource_limits.py 已有 test_tar_quota_raises_while_iterating,照同樣寫法加一支:手工組一個 .tar.gz,內含一個 PAX 擴充標頭宣告 512 MB 長度(實際資料不用真的那麼大,壓縮後只有幾 KB),extract_archive 要 raise 且測試在一兩秒內結束。nmap 與 _drain 各一支簡單斷言。
🔴 不要 poetry install、不要動共用環境、不要改 pyproject.toml。Agent 主線釘 jedi-agent-protocol==0.6.1,這版還沒推上 Nexus,共用環境裡是 0.6.0,直接跑會 ImportError(KIND_SCAN_RESULT)。用下面的 PYTHONPATH 指到套件 repo 主資料夾就是對的版本。首腦 2026-11-11 在主線 b8b1158(FR-193 rootless 已合回)用這行跑全量:2554 passed、1 skipped、0 failed,約 3 分鐘;工作區裡跑守衛:109 passed,12 秒。