iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0

前言

昨天,我把 HTTP 回應標頭與 HTML 交給 Gemini,觀察它能不能根據資料整理安全分析。

它能辨識 Cookie、搜尋表單與輸入參數,但也出現一個問題:前面把 CSP、HSTS 描述成缺少的防護,後面卻又承認,資料只是節錄,無法判斷完整回應有沒有設定。

這讓我發現,後續如果要讓程式處理這些回答,光是要求它附上證據還不夠。我還需要知道每個判斷的類型,以及引用究竟來自哪份資料。

所以第 3 天,我想先做出這個流程:

為輸入資料標記來源
        ↓
要求模型依固定格式回答
        ↓
檢查 JSON、欄位與引用
        ↓
人工核對結論是否成立

Structured outputs 解決什麼問題?

如果模型今天使用「風險」,明天改成「潛在問題」,後天又把分析寫成一整段文章,人工閱讀可能沒問題,但程式就很難固定取出需要的資訊。

Google 的 Structured outputs 可以透過 JSON Schema 約束輸出結構,例如指定欄位、資料型別,以及分類允許使用的值。不過,官方也提醒,應用程式仍要驗證內容,處理符合格式但語意錯誤的結果。

對這個專案而言,我會分成三件事:

檢查層次 要確認的問題
格式 回答能不能解析?欄位與型別對不對?
來源 引用是否真的出現在指定的證據中?
語意 這段證據足以支持這個結論嗎?

Structured outputs 主要協助第一層。後面兩層,仍然需要另外設計檢查方式(也有可能是工人智慧)。

先讓輸入資料有名字

今天沿用昨天的 HTTP 與 HTML 節錄,內容仍然是自行編寫的模擬資料,沒有實際部署,也沒有執行網站掃描。

我替兩份資料加上來源編號:

來源 內容 請求編號 是否完整
E1 HTTP 回應標頭節錄 mock-request-01 false
E2 HTML 節錄 mock-request-01 false

相同的 request_id 表示這兩份模擬資料屬於同一個請求;is_complete: false 則明確提醒模型,它拿到的不是完整內容。

HTTP 資料會包成這樣:

{
  "source_id": "E1",
  "kind": "http_response_headers_excerpt",
  "request_id": "mock-request-01",
  "is_complete": false,
  "content": "HTTP/1.1 200 OK\nServer: nginx\nContent-Type: text/html\nX-Frame-Options: SAMEORIGIN\nSet-Cookie: session=abc123"
}

只有 evidence_sources 裡面的 content 能作為網站證據,後續會依 source_id 核對每段引用。

每項判斷都使用固定欄位

報告最外層包含 scope、findings 與 limitations。每個 finding 再使用以下欄位:

欄位 用途
id 這項判斷的編號,方便定位錯誤
category 區分事實觀察、潛在風險與未知事項
claim 具體主張
evidence 來源編號與逐字引用
conditions 風險成立需要哪些條件
next_step 下一步要補充或核對什麼

其中,category 只允許三個值:

observation
potential_risk
unknown

observation 表示直接觀察到的資訊,並不是「已確認漏洞」。例如,看到表單宣告 method="GET",能確認的是表單宣告的提交方式,還不能確認後端有 XSS。

potential_risk 必須列出成立條件。unknown 則提供一個明確的位置,讓模型能回答「現有資料無法判斷」,不必為了填滿報告而猜測。

這次使用的 Schema 如下,省略了不影響結構的欄位說明:

{
  "type": "object",
  "properties": {
    "scope": { "type": "string" },
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "category": {
            "type": "string",
            "enum": ["observation", "potential_risk", "unknown"]
          },
          "claim": { "type": "string" },
          "evidence": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "source_id": { "type": "string" },
                "quote": { "type": "string" }
              },
              "required": ["source_id", "quote"]
            }
          },
          "conditions": {
            "type": "array",
            "items": { "type": "string" }
          },
          "next_step": { "type": "string" }
        },
        "required": [
          "id", "category", "claim", "evidence", "conditions", "next_step"
        ]
      }
    },
    "limitations": {
      "type": "array",
      "items": { "type": "string" }
    }
  },
  "required": ["scope", "findings", "limitations"]
}

這份 Schema 只定義基本結構。required 要求欄位存在,不代表字串一定有內容,也不代表風險一定寫了成立條件。這些規則會在本機再檢查。

在 AI Studio 設定這次實驗

今天,我先開啟 Google AI Studio 的新對話,在 Run settings 啟用 Structured outputs,再切換到 Code Editor,貼上 Schema 並儲存。

除了原本「未知不要當成不存在」的要求,我也在 System instructions 加上幾項規則:

只分析 evidence_sources 內的網站證據。

每段 evidence 都要指定 source_id。
quote 必須逐字引用該來源 content 中的連續片段。

節錄沒出現的標頭,不等於完整回應未設定。
潛在風險必須列出成立條件。
無法從資料支持判斷時,可以使用 unknown 與空 evidence。

本次實際設定如下:

項目 設定
日期 2026 年 9 月 17 日
模型 Gemini 3.8 Flash(gemini-3.8-flash)
Thinking level Medium
Structured outputs 開啟
Maximum output tokens 65536
Google Search、URL context、Code execution、Function calling 等工具 關閉

先用範例驗證接收流程

下面是我請AI協助編寫的單一判斷範例,不是 Gemini 的實測輸出:

{
  "id": "F1",
  "category": "observation",
  "claim": "提供的這一行 Set-Cookie 沒有明確列出 Secure、HttpOnly、SameSite。",
  "evidence": [
    {
      "source_id": "E1",
      "quote": "Set-Cookie: session=abc123"
    }
  ],
  "conditions": [],
  "next_step": "確認 Cookie 用途、完整設定與適用瀏覽器。"
}

這裡刻意把範圍寫成「提供的這一行」。這行資料支持對屬性文字的觀察,卻沒有提供 Cookie 被竊取、帳號遭入侵或 CSRF 已經成立的證據。

本機程式先用 json.loads() 解析完整報告,再用 jsonschema 檢查欄位與型別。確認結構正確後,才檢查來源引用。

引用檢查的核心邏輯如下,sources 是用來源編號對應到原始 content 的字典:

for finding in report["findings"]:
    for ref in finding["evidence"]:
        source_id = ref["source_id"]
        quote = ref["quote"]

        if source_id not in sources:
            errors.append("來源不存在")
        elif not quote.strip() or quote not in sources[source_id]:
            errors.append("引用未出現在指定來源")

比對必須在指定來源裡進行。即使某段 HTML 出現在 E2,把它標成 E1 的 HTTP 標頭證據,仍然應該失敗。

另外也要拒絕空字串,因為在 Python 裡,空字串會被視為任何字串的子字串。

這版程式還檢查了發現編號不能重複、觀察與風險需要引用,以及潛在風險至少要有一個非空白的成立條件。檢查失敗時回報錯誤,保留原文,不自動幫模型補寫證據。

Gemini 的實際回答

成功回答共有 5 項判斷:

編號 分類 回答內容
FIND-01 observation 觀察 Cookie 設定行未列出安全屬性
FIND-02 potential_risk 提出 Cookie 外洩風險,列出用途、XSS 與傳輸條件
FIND-03 observation 辨識使用 GET、指向 /search 且帶有 q 的表單
FIND-04 unknown 無法確認 CSP 設定
FIND-05 unknown 無法確認 HSTS 設定

完整 JSON 通過格式、引用與已實作規則的檢查。引用片段同時存在於最初資料與重新送出的資料中;我對兩份來源分別核對,都能通過。這只能確認文字對得上,無法反推模型究竟使用了哪一輪資料。

接著逐項閱讀,FIND-03 的問題就出現了。它的主張是:

網頁包含一個使用 GET 方法將查詢參數 q 傳送至 /search 的搜尋表單。

但它只引用:

<form action="/search" method="GET">

這段支持 GET 與 /search,沒有包含 q。輸入資料確實另有名稱為 q 的欄位,但模型沒有把那段一起列入證據。

比較完整的引用,應再包含有 name="q" 的 <input> 片段;若要符合今天的逐字引用規則,還要保留所選來源實際的換行。另一個做法是縮小主張,只描述目前引用能支持的內容。

Day 3:先讓錯誤可以被定位

今天的成果,是把原本整段的安全分析,拆成可以逐項檢查的資料結構。

缺少欄位時,可以指出哪個欄位;來源不對時,可以定位到哪個發現;引用沒有出現時,也可以直接回到原始資料核對。

但格式與引用都正確之後,判斷仍然可能是錯的。這個限制已經在今天的人工測試中具體呈現。

今天只取得一份成功回答,而且同時調整了提示詞、輸入格式與輸出設定,重新送出的資料也有差異。因此,可以記錄這次哪些地方符合要求、哪些地方仍需修正,但不能把差異全部歸因於 Structured outputs,更不能據此估計模型的穩定性。

第 4 天預計朝 Gemini API 串接前進,讓輸入、原始回答與驗證結果都能被保存。

那就…
明天見!


上一篇
Day 02 | Google AI 有想像中這麼聰明嗎?
下一篇
Day 04 | 串了API之後,原本Studio的功能都還在嗎
系列文
30 天打造 AI Web Security Agent:從 Google AI Studio 到 Gemini Agent 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言