第 19 章

HTTP In、Response、Request 與 API 安全

HTTP endpoint 會把不可信網路輸入帶進 Flow,HTTP Request 則把資料送往另一個服務。你要同時處理路由暴露、live request 物件、輸入契約、每條分支的回應、逾時、credentials(認證資料)、authentication(身分驗證)、authorization(授權)與 direct access(直接存取),不能只看一次成功回傳。本章只做離線檢查,不連線、不部署。

先畫清楚 HTTP 的四個邊界

Add-on 22.0.1 內嵌 Node-RED 5.0.2。HTTP In 建立伺服端路由,HTTP Response 結束同一次請求;HTTP Request 是用戶端;HA WebSocket 套件的 API node 又是另一條具有 Home Assistant 權限的路徑。它們都叫 API,但信任來源、credentials 與失敗語意不同。

功能資料方向主要風險本章處理方式
HTTP In → HTTP Response網路請求進入 Flow,再回覆呼叫者未授權存取、超大 body、路徑參數與 header 注入、漏回應固定 method/path、白名單驗證、每條路徑回應、保持停用
HTTP RequestFlow 向外部 HTTPS 服務送出請求SSRF、憑證外洩、重新導向、無界回應與逾時固定目的地、credentials 儲存區、有限逾時、範例只放 placeholder
HA API node透過既有 HA server config 呼叫 HTTP 或 WebSocket API以整合權限讀取或變更 HA;動態 method/path/data 擴大能力只供進階、已核准的白名單用途;本章不建立或執行呼叫
Webhook/WebSocket/TCP/UDP長連線或其他網路協定認證、來源、訊息大小與生命週期各自不同不可把 HTTP 規則直接外推;Webhook 見第 12 章

Add-on 宣告 host_network: true,代表容器的本機網路可達面較廣;這是能力 grant,不代表任何 Flow 已經連線。對外 HTTP、WebSocket、TCP 與 UDP 都必須固定允許目的地。先用第 18 章的有限測試策略驗證純資料邏輯,再考慮任何整合測試。

HTTP In 與 Response 是不可拆開的同一次交換

Node-RED 5.0.2 的 HTTP In 在收到請求後送出一個 message。GET 的 msg.payload 是 query object;POST、PUT、PATCH、DELETE 的 payload 是解析後的 request body(若選擇略過解析則是原始資料)。同一則訊息還有 msg.req 與 msg.res。HTTP Response 依靠 msg.res 才能回覆原連線,沒有它就只會警告沒有 response。

live object 邊界:msg.req 是目前 Express request;msg.res 包裝目前 response。它們不是可安全持久化或任意 clone 的普通 JSON。不要放進 Context、檔案、佇列、跨重啟流程或完整 Debug,也不要把整個物件回傳給呼叫者。
欄位來源/用途安全判讀
msg.reqlive request;可讀 headers、params、query、cookies 等全部視為不可信;只投影白名單欄位,不保留物件
msg.reslive response wrapper,沿原路送到 HTTP Response任何成功、拒絕與錯誤分支都要保留;只回覆一次
msg.payloadGET query 或其他支援 method 的 body解析成功不等於 shape 合法;再驗證型別、長度、範圍與未知欄位
msg.statusCodeHTTP Response 未固定 status 時的動態狀態碼只從受控映射產生,不直接信任 caller 輸入
msg.headers/msg.cookiesHTTP Response 可合併回應 headers/cookies白名單名稱和值;禁止反射 hop-by-hop、認證或未清理內容

HTTP In 編輯器提供 GET、POST、PUT、DELETE、PATCH。POST 可啟用檔案上傳;POST、PUT、PATCH、DELETE 可選擇略過 body parsing。上傳使用記憶體儲存,因此大小與檔案數必須在 route 之前受限。Node-RED 的 JSON 與 URL-encoded parser 使用 apiMaxLength,未另設時核心值為 5mb;這只是 parser 上限,不是業務 payload 的合理上限,也不涵蓋所有 proxy 或 multipart 限制。

CSV、HTML、JSON、XML、YAML parser nodes 只能處理語法;不能替你判定欄位授權、資源成本或內容可信。外部 body 應先限制 bytes 與 content type,再解析,再驗證 shape。一般 message model 可回看第 5 章。

六步完成離線設計,不啟動任何 route

  1. 寫出單一路由契約。

    在文字中固定一個 method、一個相對 path、允許的 content type、最大 bytes、必要欄位、成功 status、拒絕 status 與 timeout。路徑只使用 /PLACEHOLDER_HTTP_PATH,不填真實 host 或秘密。

  2. 畫出所有 terminal paths。

    從 HTTP In 追蹤正常、缺欄、錯型別、未授權、內部錯誤與逾時分支;每條都必須到一個 HTTP Response。禁止把 live msg.req/msg.res 放進 Delay、Context 或等待外部事件的無界路徑。

  3. 離線讀取安全範例。

    本章練習只讀:開啟 12-http-endpoint.json,確認共 4 個 nodes、HTTP In 和 HTTP Response 都是 d:true,路徑是完整 placeholder,不 Import、不 Deploy、不啟用,也不發送測試請求。README 的隔離匯入程序是另一項獨立作業,絕不代表已獲准部署或連線。

  4. 檢查暴露與認證層。

    分開記錄 Ingress 與 direct host port;若 route 可能走 direct /endpoint/,必須規劃 http_node.username/password 或受控反向代理。這組 Basic Auth 不等於細粒度 authorization。

  5. 檢查 outbound 設計。

    離線讀取 13-outbound-http-request.json;確認 URL 是 PLACEHOLDER_HTTPS_URL、HTTP Request 為 d:true、沒有 auth、TLS 或 proxy config reference,Inject 也不會在啟動時執行。

  6. 以表格完成失敗驗收。

    逐列寫出預期 status、最小 body、允許的 headers、外部 timeout 與停止條件。網路及 HA 節點仍須停用,值只用 placeholder;真正啟用須走本章之外的變更程序。

離線契約範本(不是可執行設定)
method: PLACEHOLDER_METHOD
path: /PLACEHOLDER_HTTP_PATH
request content type: PLACEHOLDER_MEDIA_TYPE
request max bytes: PLACEHOLDER_LIMIT
success: PLACEHOLDER_STATUS + 固定 shape
reject: PLACEHOLDER_STATUS + 不含內部細節
external I/O: 禁止執行

章末離線驗收

產物預期內容失敗即停止
路由契約一個 method/相對 path,並列出 media type、bytes 上限、輸入 shape、authentication、authorization、成功與拒絕 status、timeout任何欄位仍為猜測、真實 host/秘密出現在文件,或需要發送請求才能判讀
Terminal-path/status 矩陣正常、缺欄、錯型別、未授權、內部錯誤與逾時各有唯一 HTTP Response、固定最小 body 與停止條件任一路徑漏回應、重複回應、遺失 msg.res,或必須啟用 route 才能驗證

/endpoint、Ingress 與 direct port 是三個不同概念

Add-on wrapper 固定 Node-RED backend 為 127.0.0.1:46836,userDir=/config/、nodesDir=/config/nodes、flowFile=flows.json,並把 httpNodeRoot 設成 /endpoint。所以 HTTP In 內填 /PLACEHOLDER_HTTP_PATH 時,Add-on 路徑根是 /endpoint/PLACEHOLDER_HTTP_PATH。不要在節點 path 再重複填入 /endpoint。

入口固定來源行為認證與 TLS 邊界
Ingressmanifest 為 ingress=true、動態 ingress_port=0、ingress_stream=true;Ingress NGINX 只允許 Supervisor ingress source 再 proxy backend由 Supervisor 路徑承接;不能據此推論 direct port 或自訂 route 都受同一保護
Direct listenercontainer 80/tcp 可映射到 host 1880;是否公開取決於 Supervisor port 設定ssl、certfile、keyfile只控制 direct listener TLS;不是自動憑證簽發/續期
Direct /預設透過 Supervisor /auth 保護 editor path不要啟用 leave_front_door_open;editor auth 不等於 endpoint auth
Direct /endpoint/NGINX 明確直接 proxy backend,不走 editor 的 Supervisor auth_request以 Add-on http_node Basic Auth 或受控 proxy 保護;仍需 authorization
Static content由 Node-RED static route 提供http_static.username/password只保護 static content,不保護 editor、HTTP nodes 或 Dashboard

http_node.username/password 設定存在 username 時,wrapper 用 bundled [email protected] 對 password 建 hash,交給 Node-RED httpNodeAuth。bcryptjs 是 wrapper 支援 library,不是 Palette node。Basic Auth 只回答「是否持有共同 credentials」;若不同使用者只能存取不同資源,Flow 還要使用可信身分來源做 authorization,且錯誤回應不可暴露判斷細節。

每條分支都回應,而且只回應一次

Switch 的每個 output、Catch 的每個有限錯誤分支、驗證失敗與正常分支都要到 HTTP Response。若分支丟棄訊息,caller 會一直等待到 upstream proxy 或 client timeout;若兩條分支都回覆同一個 msg.res,則會造成重複送出。安全結構是先建立固定 error envelope,再在單一回應匯合點設定受控 msg.statusCode 與 headers。

// 純資料示意;不得接到已啟用 HTTP In
// 前提:上游仍保留原本 live msg.res
const value = msg.payload && msg.payload.value;
if (typeof value !== "string" || value.length < 1 || value.length > 64) {
    msg.statusCode = 400;
    msg.payload = { code: "INVALID_INPUT" };
    return msg;
}
msg.statusCode = 200;
msg.payload = { accepted: true };
return msg;

這段只示範 shape guard,不處理身分或實際 route。HTTP Response 若節點本身固定 status,固定值優先;否則讀 msg.statusCode,最後才是 200。非 Buffer 的 object payload 一律呼叫 Express res.jsonp(msg.payload),因此 query 中的 JSONP callback 可能改變 response envelope。若契約要求不變的 JSON,必須把 callback query name 保留給安全檢查並拒絕 caller 提供該參數;另一做法是先序列化可信 JSON,再設定固定 JSON content type,避免走 object 的 JSONP 語意。

HTTP Request:固定 HTTPS 目的地,不讓 message 決定網路

HTTP Request 5.0.2 的編輯欄位包含 Method(GET、POST、PUT、DELETE、HEAD 或由 msg.method 指定)、URL、GET payload 處理方式、TLS config、Basic/Digest/Bearer 三種 credentials、keep-alive、HTTP proxy config、錯誤輸出、insecure HTTP parser、回傳型別與 headers。節點 URL 若包含 Mustache 標記,runtime 會以整則 message render;所以看似寫在節點內也不代表固定。範例只使用完整、沒有 {{...}} 的 non-templated placeholder URL,正式目的地則必須另經核准。

欄位建議界線原因
Method/URL節點固定 method 與經核准的完整 HTTPS URL;URL 不使用 Mustache,也不接受不可信 msg.url/msg.method防止 SSRF、內網探測與非預期 write method
PayloadGET 預設 ignore;若 query/body 另行需要,白名單 key 並限制長度。其他 method 固定 content type 與 shape避免把完整 message 或未驗證 object 序列化送出
Authentication使用 node credentials 欄位;URL、Function、header 常數與匯出 JSON 都不嵌入 credentialsBasic/Digest/Bearer 的 secret 都不應進 flow source 或 Debug
TLS選用受控 TLS config,驗證伺服器憑證與 hostname;不得讓 message 關閉驗證沒有選 TLS config 時,msg.rejectUnauthorized 仍可控制驗證,甚至設為 false
Proxy使用 HTTP Proxy config node 並固定允許出口;同時檢查 proxy credentials 與 NO_PROXY 邊界proxy 改變實際目的地、DNS 與信任終止點
Parser/redirectinsecureHTTPParser=false;由受控 msg.followRedirects=false 停用 redirect,或交由 egress proxy/upstream 強制目的地政策本節點沒有 redirect-host allowlist;跟隨跨 host redirect 會擴大 SSRF
Return只選 text、binary buffer 或 parsed JSON 中需要的一種;在 egress proxy/upstream 強制 response 大小節點沒有每次 response bytes 上限;大回應會耗用記憶體

除了 node 設定,runtime 還會消費 msg.headers、msg.cookies、msg.followRedirects,以及 method、URL、timeout 控制 msg.method、msg.url、msg.requestTimeout;未選 TLS config 時還會消費 msg.rejectUnauthorized。因此緊接節點前要建立只含核准 payload、必要 header 與固定控制值的全新 allowlisted message,不能把 HTTP In、Dashboard 或 MQTT message 原樣傳入,也不能只刪一兩個已知欄位。

核心 runtime 的 HTTP Request timeout 未另設 httpRequestTimeout 時為 120000 ms,也可由正數 msg.requestTimeout 覆寫;程式另設最多 21 次 redirects。這些是 5.0.2 行為,不是所有服務都適合的政策。對每個目的地應設定更窄且有理由的 timeout,並在上游限制重試次數與總期限。Redirect 應停用,或由 egress proxy/upstream 同時執行目的 host allowlist 與 response-size 上限;不要用無限重試補救 timeout。

核心會先把完整 response buffer 放入記憶體;選 parsed JSON 時,還會先執行 JSON.parse,之後才輸出 message。HTTP Request 本身沒有每次 response-byte limit 或 redirect-host allowlist,所以節點後才檢查 msg.responseUrl、content type 或 body 大小,無法阻止已發生的 SSRF、跨 host redirect 或記憶體耗盡。成功輸出仍應白名單處理 msg.statusCode、msg.headers、msg.responseUrl、msg.payload、msg.redirectList 與可能的 msg.responseCookies,但這只保護下游資料使用;不要把 headers/cookies 原樣接回 HTTP Response,也不要完整 Debug。

安全範例狀態:13-outbound-http-request.json 的 URL 只含 PLACEHOLDER_HTTPS_URL,沒有 embedded auth,HTTP Request 固定 d:true。這是閱讀用 schema,不提供可連線服務;不得替換、啟用或 Deploy。

method、path、body、headers 要分層驗證

「HTTP In 已選 POST」只完成 router 層 method 比對。完整驗證至少有傳輸層、語法層、shape 層、授權層與業務層;任何一層失敗都回傳有限、穩定的錯誤,不附 stack、token、內部 host 或原始 body。

輸入面正向允許清單拒絕案例
Method每個 route 只接受節點已宣告 method以 query 欄位要求切換成 PUT/DELETE;重送非 idempotent method
Path/params固定 segment;參數限制字元集、長度與已授權資源範圍..、encoded separator、空 segment、過長 ID、未授權 object ID
Query/body必要 keys、精確型別、範圍、array 上限、拒絕未知 keysnull、字串代替數字、深層 object、巨大 array、重複 key
Headers固定 content type、有限 accept;只採信受控 proxy 清理後的身分 header多個矛盾 content-length、動態 authorization、偽造 forwarding headers
回應固定 status map、content type、cache policy 與最小 body反射 request headers/body、回傳 exception、依呼叫者輸入設定 Location

CORS 不是 authentication。Node-RED 可透過全域 httpNodeCors 設定為 HTTP nodes 增加 CORS middleware,並對 OPTIONS 設定處理;HTTP In 節點 dialog 沒有逐 route CORS 欄位。只允許明確 origin、method 與 headers,不使用寬鬆 wildcard 搭配 credentials。瀏覽器同源政策也不會阻止非瀏覽器 client。

反向代理要限制 request body、header bytes、連線數與 upstream timeout,並只信任代理自己覆寫的 forwarding headers。TLS 終止在哪一層、proxy 到 backend 是否仍受保護、client IP 如何取得都要記錄。Add-on direct TLS 只涵蓋 direct listener;Ingress 與外部 proxy 是不同 TLS session。

有限錯誤模型

400 INVALID_INPUT    欄位或型別不合契約
401 AUTH_REQUIRED    缺少或無效 authentication
403 NOT_ALLOWED      身分有效但沒有該資源權限
404 NOT_FOUND        不揭露不存在或不可見資源細節
413 BODY_TOO_LARGE   在解析前拒絕超過上限
415 MEDIA_TYPE       不支援的 Content-Type
429 RATE_LIMITED     有界速率限制;不含內部計數細節
500 INTERNAL_ERROR   固定最小 body;詳細原因只進受控 log
504 UPSTREAM_TIMEOUT outbound 期限已到;不無限重試

HA API node 是進階權限入口,不是一般 HTTP 捷徑

HA WebSocket 0.80.3 註冊的 persisted type 是 ha-api。它可選 WebSocket 或 HTTP;HTTP method 為 GET、POST、PUT、DELETE,path 會移除開頭的 /api/ 再交給既有 HA server config。WebSocket data 必須包含 type。Data 可用 JSON/JSONata;輸出位置與型別也可自訂。這些動態能力讓一個 node 能跨越多種 HA API,不等於呼叫都只讀。

0.80.3 沒有 Block Input Overrides:ha-api 會從 msg.payload.protocol、msg.payload.method、msg.payload.path、msg.payload.data、msg.payload.dataType、輸出位置 msg.payload.location、輸出位置型別 msg.payload.locationType、回應型別 msg.payload.responseType 與輸出屬性 msg.payload.outputProperties 讀取 overrides。編輯器中固定欄位並不是 allowlist 邊界。緊接 API node 前必須刪除並重建完整 msg.payload,只加入核准值;若無法做到,就把 node 與所有不可信輸入完全隔離。只刪 method/path 不足以封鎖其他 overrides。
能力風險必要限制
Protocol/HTTP method/pathmessage 可改協定、讀取,也可能寫入、刪除或觸發狀態變更重建完整 msg.payload,只留下單一核准 protocol、method 與 path;否則隔離不可信輸入
WebSocket type/data低階命令範圍廣,message 可改 data 與 dataType固定命令類型與精確 schema;禁止把 caller object 直接傳入
Output controlsmessage 可改 location/locationType、responseType、outputProperties,擴大資料寫入或暴露輸出目的與型別列入同一 allowlist;不保留 caller 提供的 output controls
HA server config承接整合連線與 token 權限不匯出 token;分開 production/test config,使用最小權限
Debug enabled/results可能顯示 method、path、data 或 HA response保持關閉;僅輸出白名單結果,清理 entity/device/area 等環境 ID

若已有明確用途的 Current State、Get Entities 或 Action node,優先使用語意更窄的節點;Action 的 target 與 data 邊界見第 11 章。API node 只適合沒有專用 node 且已確定 HA API 契約的進階案例。本章不提供可執行 HA API flow、method、path 或 data,也不指示啟用任何 node。

ha-webhook 是 HA 事件入口,不是 Node-RED HTTP In 的別名。Webhook URL/ID 是敏感入口值,不得出現在截圖、flow export 或一般 log;外部來源、重放、payload 大小與後續副作用仍要限制。其他 WebSocket、TCP、UDP nodes 有各自的 config 與 framing 語意,不能套用 HTTP Response 配對模型。

更完整的 credentials、proxy、TLS、host network 與 least privilege 維運界線見第 21 章。發生 timeout、記憶體或連線問題時,依第 22 章收集有限資料,不要先打開完整 Debug。

故障排除

  • HTTP In 有訊息,但 caller 一直等待:逐條追蹤 Switch、驗證拒絕、Catch 與正常路徑是否都保留原 msg.res 並抵達 HTTP Response。不要以 Delay 拉長等待;先保持 route 停用,在純資料副本驗證 terminal path。
  • HTTP Response 顯示沒有 response:這則 msg 不是來自同一次 HTTP In,或途中重建 message 遺失 msg.res。不要自己仿造 msg.res;改為保留原 message,只覆寫受控欄位。
  • Ingress 可開啟,direct endpoint 卻是 401/不可達:兩者是不同 listener 與認證邊界。檢查 host port 是否映射、direct TLS 與 http_node 設定;不要為排錯啟用 leave_front_door_open。
  • endpoint 可達但沒有要求 editor 登入:direct NGINX 對 /endpoint/ 本來就不走 editor 的 Supervisor auth。設定 endpoint 專用認證與 authorization,不要把網路位置當成存取控制。
  • HTTP Request timeout 或反覆 redirect:保持節點停用,檢查 placeholder 是否尚未替換、DNS、proxy、TLS chain、最終 host、有限 request timeout 與 redirect policy。不要關閉 certificate validation 或設無限重試。
  • parsed JSON 看似成功但後續出錯:JSON parser 只保證語法。增加必要欄位、型別、範圍、深度、array 長度與未知欄位拒絕;錯誤路徑仍要回應。
  • CORS 錯誤只在瀏覽器出現:確認 origin、preflight method/headers 與 proxy 回應;不要用 wildcard 加 credentials,也不要把通過 CORS 當成已 authentication。
  • HA API node 顯示 requires path/type:這是進階 node 的輸入契約錯誤。不要從外部 payload 臨時補值;保持停用,回到固定 HTTP path 或 WebSocket type 的白名單設計。

固定版本來源與官方文件

本章以 exact commits 判定 Add-on 路由與 Node-RED/HA nodes 行為;rolling 文件只作一般使用說明。固定來源沒有證明的 proxy 或環境政策,本章不作推論。

常見問題

HTTP In 可以不接 HTTP Response 嗎?
不應如此。HTTP In 建立一個等待回覆的 request;每個正常、拒絕與錯誤 terminal path 都要保留同一個 live msg.res 並送到 HTTP Response,而且只能回覆一次。長時間工作應改成明確的非同步 API 契約,不要讓連線無界等待。
從 Home Assistant Ingress 打開 editor,是否表示 /endpoint 自動受相同登入保護?
不是。Ingress、direct editor 與 direct /endpoint/ 是不同路徑。固定 direct NGINX 對 endpoint 不套 editor 的 Supervisor auth_request;你需要 http_node 或受控 proxy,並另外做 authorization。
可以把完整 URL 放在 msg.url,讓同一個 HTTP Request 呼叫多個服務嗎?
不適合不可信輸入。動態 URL 會形成 SSRF 與內網探測面;每個 node 固定經核准的 HTTPS 目的地,或在呼叫前把有限代號映射成 server-side allowlist。URL 不能含 username、password、token 或其他 credentials。
CORS 允許某個網站後,是否就完成 API 認證?
沒有。CORS 是瀏覽器跨來源讀取規則,不是 authentication 或 authorization,也擋不住非瀏覽器 client。你仍需要 endpoint credentials、資源授權、TLS、rate limit 與輸入驗證。
為什麼不直接用 HA API node 取代所有 HA nodes?
ha-api 可動態選 HTTP/WebSocket 與 data,權限與輸入面較廣。專用 node 的語意通常更窄、更容易驗證。只有沒有專用能力且 API 契約已固定時才考慮進階 API node,本章不提供可執行呼叫。
第三方可能重送同一個 POST 時,離線契約要先決定什麼?
先決定該操作能否安全重複、使用哪個受控 idempotency key、key 的保存期限,以及重複請求要回傳哪個固定 status/body。若副作用不可安全重複且沒有去重界線,就停止設計;不能靠 QoS、timeout 或呼叫者「應該只送一次」補足。