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 Request | Flow 向外部 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。
msg.req 是目前 Express request;msg.res 包裝目前 response。它們不是可安全持久化或任意 clone 的普通 JSON。不要放進 Context、檔案、佇列、跨重啟流程或完整 Debug,也不要把整個物件回傳給呼叫者。| 欄位 | 來源/用途 | 安全判讀 |
|---|---|---|
msg.req | live request;可讀 headers、params、query、cookies 等 | 全部視為不可信;只投影白名單欄位,不保留物件 |
msg.res | live response wrapper,沿原路送到 HTTP Response | 任何成功、拒絕與錯誤分支都要保留;只回覆一次 |
msg.payload | GET query 或其他支援 method 的 body | 解析成功不等於 shape 合法;再驗證型別、長度、範圍與未知欄位 |
msg.statusCode | HTTP Response 未固定 status 時的動態狀態碼 | 只從受控映射產生,不直接信任 caller 輸入 |
msg.headers/msg.cookies | HTTP 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
- 寫出單一路由契約。
在文字中固定一個 method、一個相對 path、允許的 content type、最大 bytes、必要欄位、成功 status、拒絕 status 與 timeout。路徑只使用
/PLACEHOLDER_HTTP_PATH,不填真實 host 或秘密。 - 畫出所有 terminal paths。
從 HTTP In 追蹤正常、缺欄、錯型別、未授權、內部錯誤與逾時分支;每條都必須到一個 HTTP Response。禁止把 live
msg.req/msg.res放進 Delay、Context 或等待外部事件的無界路徑。 - 離線讀取安全範例。
本章練習只讀:開啟 12-http-endpoint.json,確認共 4 個 nodes、HTTP In 和 HTTP Response 都是
d:true,路徑是完整 placeholder,不 Import、不 Deploy、不啟用,也不發送測試請求。README 的隔離匯入程序是另一項獨立作業,絕不代表已獲准部署或連線。 - 檢查暴露與認證層。
分開記錄 Ingress 與 direct host port;若 route 可能走 direct
/endpoint/,必須規劃http_node.username/password或受控反向代理。這組 Basic Auth 不等於細粒度 authorization。 - 檢查 outbound 設計。
離線讀取 13-outbound-http-request.json;確認 URL 是
PLACEHOLDER_HTTPS_URL、HTTP Request 為d:true、沒有 auth、TLS 或 proxy config reference,Inject 也不會在啟動時執行。 - 以表格完成失敗驗收。
逐列寫出預期 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 邊界 |
|---|---|---|
| Ingress | manifest 為 ingress=true、動態 ingress_port=0、ingress_stream=true;Ingress NGINX 只允許 Supervisor ingress source 再 proxy backend | 由 Supervisor 路徑承接;不能據此推論 direct port 或自訂 route 都受同一保護 |
| Direct listener | container 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 |
| Payload | GET 預設 ignore;若 query/body 另行需要,白名單 key 並限制長度。其他 method 固定 content type 與 shape | 避免把完整 message 或未驗證 object 序列化送出 |
| Authentication | 使用 node credentials 欄位;URL、Function、header 常數與匯出 JSON 都不嵌入 credentials | Basic/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/redirect | insecureHTTPParser=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。
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 上限、拒絕未知 keys | null、字串代替數字、深層 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,不等於呼叫都只讀。
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/path | message 可改協定、讀取,也可能寫入、刪除或觸發狀態變更 | 重建完整 msg.payload,只留下單一核准 protocol、method 與 path;否則隔離不可信輸入 |
WebSocket type/data | 低階命令範圍廣,message 可改 data 與 dataType | 固定命令類型與精確 schema;禁止把 caller object 直接傳入 |
| Output controls | message 可改 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 或環境政策,本章不作推論。
- Home Assistant Community App: Node-RED 22.0.1 固定提交:
/endpoint、direct/Ingress NGINX、host port、http_node、http_static與 bundled bcryptjs。 - Node-RED 5.0.2 固定提交:HTTP In/Response/Request、HTTP Proxy、TLS、parser nodes、live objects、CORS 與其他 network nodes。
- HA WebSocket nodes 0.80.3 固定提交:
ha-api與ha-webhook的註冊及輸入契約。 - Node-RED 官方文件:Core nodes。
- Node-RED 官方文件:Messages。
- Node-RED 官方文件:Securing Node-RED。
- Add-on 官方文件(固定提交)。
- 本站停用範例:12-http-endpoint.json 與 13-outbound-http-request.json;請先讀安全範例說明。
常見問題
HTTP In 可以不接 HTTP Response 嗎?
msg.res 並送到 HTTP Response,而且只能回覆一次。長時間工作應改成明確的非同步 API 契約,不要讓連線無界等待。從 Home Assistant Ingress 打開 editor,是否表示 /endpoint 自動受相同登入保護?
/endpoint/ 是不同路徑。固定 direct NGINX 對 endpoint 不套 editor 的 Supervisor auth_request;你需要 http_node 或受控 proxy,並另外做 authorization。可以把完整 URL 放在 msg.url,讓同一個 HTTP Request 呼叫多個服務嗎?
CORS 允許某個網站後,是否就完成 API 認證?
為什麼不直接用 HA API node 取代所有 HA nodes?
ha-api 可動態選 HTTP/WebSocket 與 data,權限與輸入面較廣。專用 node 的語意通常更窄、更容易驗證。只有沒有專用能力且 API 契約已固定時才考慮進階 API node,本章不提供可執行呼叫。