iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
AI Engineering

AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄系列 第 25

Day 25|登入只是第一關:誰能接手 AI Session,營運者又看得到什麼?

  • 分享至 

  • xImage
  •  

簡短回顧

昨天把整套的形狀講完了,其中有一條分界線今天要接著講:終端的資料流不經過控制平面,一旦接起來,中間就不會再回頭問任何人。

所以「這一場是不是你的」這個問題,必須在連線交出去之前就問完。

今天講的是身分:這套替我跑 code review 的設施,誰看得到哪一場、誰能開一場。

昨天那段錄影再放一次,這次看的是別的地方:畫面最前面是登入頁,而 Session 列表裡的每一張卡片,等一下都要回答「這一場是不是你的」。

claude-pty 67 秒實機 Demo:登入後建立 Session,在瀏覽器操作 Claude Code,再終止容器

點圖可以到 YouTube 看原尺寸

先把結論放前面:隔離不等於我碰不到。 一般使用者之間的狀態與憑證可以彼此隔開,但應用程式管理員仍能依角色權限查看與操作所有 Session;而這個服務要替人啟動 CLI,部署營運者也必須讓執行中的程式在建立 Session 時取得那個人的 token 明文。兩種角色都仍在信任鏈裡,後面每一節做的事,都是在這條線裡面做的。

今天其實要回答三個不同問題:登入怎麼認出你是誰、連線交出去以前怎麼確認這一場是不是你的,以及登入失效後,已經接起來的線要怎麼收。

這一天看起來像在談一般網站的登入,但登入後交出去的不是一頁資料,而是一個已經取得工作目錄存取權、模型憑證與工具能力,而且即使瀏覽器關掉仍可能繼續工作的 AI Agent。誰能接上哪一場 Session,實際上就是誰能接手那個 Agent 當下擁有的能力;因此登入、Session ownership 與撤銷,都是 AI 執行邊界的一部分。

這三件全部做完,也不能推出管理員看不到你的 Session,更不能推出部署營運者碰不到你的 token。

先把這篇會出現的三種角色分開:

角色 實際邊界
一般使用者 只能查看與操作自己的 Session;拿別人的編號查詢時,得到的回應與編號不存在相同
應用程式管理員 可以管理帳號,也能查看與操作所有人的 Session
部署營運者 能碰主機、資料庫與服務金鑰,因此有能力解開 token,仍在憑證的信任鏈裡

因此這一層失效的樣子很具體:一般使用者看見了別人的 Session,或者自己的憑證替別人跑了一場。管理員與部署營運者則不是繞過隔離,而是原本就擁有較大的權限;系統必須把這件事明寫出來,不能拿「使用者之間已隔開」暗示他們也被排除。今天講的東西主要分散在 claude-pty/server/auth.pyclaude-pty/server/app.py:密碼怎麼雜湊、帳號怎麼正規化在前者;cookie 簽入 uidpwv 與驗證 gate 則在後者。

登入只是第一關:過了登入門之後還有「這一場是不是你的」的檢查哨,線一接起來就不再問,而營運者手上握著所有人的 token

接上網路之前,這些都不存在

這個工具原本只跑在我已登入的個人機器上,所以它沒有自己的登入層。作業系統已經知道坐在鍵盤前的是我;檔案與容器也都在我的使用者權限下。當時只有我會操作它,應用程式不需要再回答誰能做什麼。

東西一放到瀏覽器後面,這些全部要重新長出來。而且不是「加一個登入頁」就結束,因為這個系統有兩條路,兩條都要守。

先隔開狀態與身分

「外面的人進不進得來」是後面幾節的事。先答另一個問題:進得來的人之間呢?

接回昨天那句定位。每一場 Session 只供一人操作,但平台本身支援多位彼此隔離的使用者;它一旦上了網路,這些邊界就不能再省略。

要隔的是兩樣東西。

狀態這一層:每個人有自己的一塊空間。設定目錄整個指過去,對話紀錄、設定、技能檔案都跟著走,錄下來的流量也分開放。所以一般使用者開自己的 Session,不可以翻到別人的對話;應用程式管理員能跨帳號查看 Session,則是明確存在的角色權限,不是這層隔離承諾要擋住的對象。

身分這一層:每個人在設定頁貼自己的 CLI 授權 token,也就是官方那支指令產出來、代表「我這個帳號授權這台機器用它」的那一串。它加密存進資料庫。建立容器時,控制平面預設先把 token 放進容器裡的一次性檔案;entrypoint 隨即刪除檔案路徑,把內容轉進匿名 pipe,再透過檔案描述符交給 Claude Code。環境變數注入仍保留為退路,但不是預設。那一場就是用貼的人自己的額度在跑,沒有貼的人開不了場。

這兩層合起來,能讓對話紀錄、設定、錄製檔案與模型憑證不在一般使用者之間混用。不過這還不是完整的租戶隔離:應用程式管理員本來就能跨帳號管理 Session;共用的 Jaeger 查詢介面也尚未加上認證,能連上它的使用者仍看得到跨使用者的 trace 中繼資料。這些限制都必須與一般使用者之間的隔離分開說。

說是隔開了,那我算什麼?

開頭那句「隔離不等於我碰不到」,理由就在這裡。

所有人的那串授權 token 都加密躺在我的資料庫裡,用的是同一把金鑰,而那把金鑰是從整個系統的那一個秘密導出來的。拿到設定檔,再拿到資料庫,全部都解得開。而那串東西的效力是:拿著它就能以那個人的身分呼叫模型、燒他的額度。

這不是實作沒做好,是這個工具的定位決定的。它是一個「幫你把環境開起來、幫你把 CLI 跑起來」的東西,那它就非得拿到那串授權不可,不然它跑起來的是一個沒有授權的殼。定位在這裡,這條性質就跟著在這裡。

那有沒有更好的做法?有幾條,但每一條都要看清楚它實際擋掉了什麼、擋不掉什麼。

一、不落地。 每開一場現貼一次,資料庫完全不存。這確實拿掉了「一次全拿」那個規模,但每一場都要重貼,而它沒有解決根本問題(下面會講為什麼)。

二、用使用者自己的密碼導出金鑰。 密文只有在使用者提供密碼後才解得開,單拿資料庫與設定檔還不夠。這在靜態資料上有效,但忘記密碼就連 token 一起救不回來;而服務真正要使用 token 時,仍然得取得明文,因此控制這段程式碼的人仍在信任邊界裡。

三、把金鑰交給外部的金鑰管理服務。 金鑰不再躺在設定檔裡,解密權限可以由政策限制,操作也能留下稽核紀錄。它能不能阻擋營運者,取決於權限怎麼切:如果服務身分具備解密權限,而營運者又能控制程式碼與該身分,KMS 不會自動把營運者移出信任鏈,但仍增加了金鑰分離、存取政策與稽核能力。

在我目前這套「由服務代為啟動 CLI,並把 bearer token 交進容器」的架構裡,有一件事繞不過:服務在建立 Session 時必須取得 token 明文,否則容器裡的 CLI 就沒有授權。因此,能控制執行中程式碼與服務身分的人,仍有機會碰到明文。前面幾種存放方式能降低 token 靜置時被整批取走的風險,也能增加權限限制與稽核能力,但不能單靠它們消除使用當下的暴露面。

而要把我這個營運者移出信任鏈,最直接的方法就是自己跑一份自己的。這也是我把它公開出來的理由之一:你可以先檢查程式碼,再自己跑一份,不必把模型憑證交給我營運的服務。自行部署能把我移出執行期的憑證信任邊界,但程式碼、映像與更新是否可信,仍要由部署的人自行驗證。

在那之前,能做的只有講明白。所以說明文件裡講邊界的那一節,第一句就是這個:開帳號給誰,就等於請他信任你。

順帶一提,那個秘密身上背著兩件事:換掉它,所有人要重新登入,而且所有人的 token 一起解不開。這件事我在設定範例旁邊標了。

知道了這條界線在哪,接下來才是實作:怎麼認出進來的人、密碼怎麼存,以及中間踩過的坑。

授權掛在哪裡?

先回答第二個問題:連線交出去以前,怎麼確認這一場是不是你的。

管理畫面的殼由 Nginx 直接送出;真正讀寫資料的 API 才進 Flask,由 Flask 驗證身分與權限。麻煩的是終端那一路:Nginx 要把連線交給 ttyd。兩顆 ttyd binary 共同的第一道檢查仍在 Nginx;Rust 版還會透過 --auth-url 再問一次控制平面,這一層縱深防禦留到明天談。兩道檢查都發生在連線建立以前,不代表已經接起來的 WebSocket 會持續重新驗證。

所以驗證要發生在 Nginx 把連線交出去之前。Nginx 有一個機制叫 auth_request:在真的轉發之前,先發一個內部子請求去問另一個地方「這個人可以嗎」,回 200 才繼續。

問的對象就是控制平面。它同時驗兩件事:cookie 有效嗎(你是誰),以及依照帳號角色能不能碰這一場。對一般使用者而言,第二題就是「這一場是不是你的」;管理員則依明確的角色權限放行。

一般使用者答不對第二題時,Nginx 不會給一頁 403,而是把人送回管理畫面。API 那一面更徹底:一般使用者拿別人的 Session 編號去問,回的是查無此物,跟「這個編號根本沒存在過」是同一句話。不然的話,光看回的是「不准」還是「沒有」,就能問出哪些編號是真的。

進得來的人之間,隔到什麼程度:狀態一層、身分一層,驗證發生在 Nginx 交出連線之前

這裡還疊了一道縱深防禦:Session 編號是隨機產生的,不帶順序或身分資訊,讓人不容易沿著流水號逐筆探測。但因果不能倒過來:真正的邊界仍是角色與擁有權檢查。即使編號被猜到或從別處洩漏,一般使用者拿別人的編號與拿一個不存在的編號來問,都應該得到同一句「查無此物」。隨機編號降低的是探測機會,不是拿來代替授權。

還有一個附帶好處。ttyd 的 port 是動態分配的,Nginx 不能寫死。而子請求的回應標頭可以被抓成 Nginx 的變數,變數又能用在轉發目標上。所以控制平面在放行的同時,順手把「這一場的 port 是幾號」寫在回應標頭裡帶回來。

整條路寫出來就這幾行:

location ~ ^/session/(?<sid>[A-Za-z0-9]+)/ {
    set $session_id $sid;                                    # 先在父請求把編號求值
    auth_request /_auth_view;                                # 問控制平面:這場是不是他的
    auth_request_set $ttyd_port $upstream_http_x_ttyd_port;  # 把回應標頭抓成變數
    resolver 127.0.0.11 valid=10s ipv6=off;                  # Docker 內建 DNS
    set $ttyd_upstream control;                              # 執行期解析的服務名
    proxy_pass http://$ttyd_upstream:$ttyd_port;             # 執行期才決定的目標
}

這裡的 resolver 不是裝飾。Nginx 官方文件說明,proxy_pass 含變數時,網域名稱會改在執行期解析;容器化部署因此要把 Docker 的內建 DNS 127.0.0.11 告訴 Nginx。如果 Nginx 直接跑在 Host,目標改成字面 IP 127.0.0.1,這一行就可以拿掉。

結果是三層疊在一起:port 不出現在任何網址、授權發生在轉發之前、而那個 port 只發布在容器之間的內部網路上,沒有對外開。

第三層要講清楚它的界線:擋得住的是從外面進來的連線,擋不住同一個內部網路上的其他容器。這個網路上有誰,是部署的人決定的。

三道疤

下面三道疤都發生在這條路上:擁有權檢查必須掛在真正的終端入口前面,而那條路同時跨過 Nginx、控制平面與瀏覽器。上面那段講得很順,實際上是踩出來的。

第一道:全部 403。

第一版是用 Nginx 的 map 從網址裡抽出 Session 編號。map 是 Nginx 的一個設定指令,意思是「這個變數的值,請照這張對照表,從另一個變數換算出來」。我要它從網址算出編號,再把編號帶去問控制平面。

結果控制平面收到的永遠是空的,每一場都被擋。

這裡要先分清楚兩個請求。瀏覽器打進來的那一個叫父請求;auth_request 為了去問控制平面而生出來的那一個叫子請求。它們是兩個不同的請求,各自有自己的網址。

map 是延遲求值的:宣告的時候不算,等到第一次有人讀那個變數才算。那第一個讀它的是誰?是子請求,因為要組出「去問控制平面」的那個網址。所以真正動手算的那一刻,手上的網址是驗證端點自己的網址,不是使用者進來的那一條。抽出來當然是空的。

解法是在父請求就先把它算好、存進變數。父子共用同一份變數容器,算過一次就記在那裡,子請求讀到的就是父請求算出來的那個值。

第二道:驗證端點不能直接對外開放。

它只該由 Nginx 的子請求打,外面沒有任何理由碰它。而且它有副作用:發現這一場沒有存活的終端時,會當場開一個。

所以 Nginx 把子請求位置標成 internal,並讓對外打到相應路徑的請求一律收到 404。真正的邊界是外部請求無法抵達後面的處理流程;404 只是這支內部端點對外採用的回應,不是拿來保密端點名稱。

第三道:測試全綠,終端卻是一片空白。

管理畫面裡的終端是用同源的 iframe 嵌進來的。而終端那條路由的內容安全政策,是 Nginx 在轉發的時候掛上去的,不是 Flask 給的(Flask 對全站掛的是「一律禁止嵌入」,但終端這條根本不經過 Flask,所以掛不到它頭上)。

Nginx 掛的那一條只能寫「允許同源嵌入」,不能跟著寫「一律禁止」。

寫成禁止的話,抽屜拉開來是一片空白,只有瀏覽器的 console 有一行錯誤。所有測試照樣綠。

要補充的是,cookie 的 SameSite=Lax 可以阻止跨站 iframe 帶著登入 cookie 進來,但「同站」不等於「同源」:不同子網域,甚至 localhost 的不同 port,都可能仍被視為同站。因此 frame-ancestors 'self' 有自己獨立的工作:只有同源的管理畫面能嵌入終端。它和 cookie 限制疊在一起,但兩者擋的不是同一條路。

這種坑最難防,因為它不會失敗,它只是不動。

登入狀態放在哪

回到第一個問題:登入之後那個「你已經是誰」的狀態,我沒有存在伺服器上。

先回答一個會被問的問題:這裡沒有用 JWT,用的是 Flask 內建的那張簽章 cookie。伺服器不記得任何人登入過,只是每次收到請求時驗一下這張 cookie 的簽章對不對。好處是多開幾個 worker 也不用共享什麼,它們拿著同一個秘密,自然就互相認得對方發出去的 cookie。

要講清楚的是簽章不是加密。裡面的內容攤開就看得到,簽章保證的只有「沒有被人改過」。所以它裡面只放得下帳號 id 跟下面要講的那個版本號,秘密一律不放。真的對外的時候,還要記得讓這張 cookie 帶上只走加密連線的旗標,這一項系統啟動時會自己檢查並提醒,因為它太容易忘了。

麻煩的是撤銷。伺服器上沒有一份「目前有效的登入」清單,所以沒有東西可以刪。

解法是在帳號上放一個版本號,簽進 cookie 裡。要讓舊的 cookie 全部失效,就把帳號上的版本號加一,之後每一張舊 cookie 帶的版本都對不上。

改了密碼之後,那條已經連上的線

接著是第三個問題:登入失效後,已經接起來的線要怎麼收。

改密碼的時候,系統就是把那個版本號加一,這個帳號所有既有的簽章 cookie 當場失效,包括你正在操作的這一台,畫面直接把你送回登入頁。

這裡其實有一個選擇。可以特別把「按下送出的這一台」續上,讓操作的人不用重登,畢竟他剛剛才用舊密碼證明過自己是本人。

我不做這個例外。改密碼的語意講成一句話就好:這個帳號既有的登入狀態全部失效,已經開著的終端連線也盡力切斷。 要繼續用就重新登入,那本來就是你剛剛那個動作的意思。多一個例外就多一件要記得的事,而它省下的只是少按幾個鍵。

但 cookie 不是真正的問題。那個版本號對一條已經升級完成的 WebSocket 沒有任何效果。

那條分界線在這裡咬回來了。授權只發生在連線交出去之前,之後那條線就是一條線,不會再有人回頭問它「你還算數嗎」。所以就算把 cookie 全部作廢,一個已經打開的終端分頁,還是一個能打字的 shell。換密碼的理由如果是「我懷疑登入狀態被盜了」,只作廢 cookie 還不夠。

因此還得主動去收。改密碼的處理裡有一步,是把這個人所有活著的終端直接切斷。

而因為登入狀態本來就全部作廢了,這一步不必去分辨「哪一條連線屬於哪一台裝置」,那件事在伺服器這邊本來也分不出來。讓既有登入失效、主動切斷已知連線,是同一個決定的兩半。容器不受影響,重新登入、抽屜重開,接回的還是同一場。

這裡要分開兩件事:收回人的操作權,不等於停止 Agent 的執行能力。 現在這版切斷的是前者;Session 容器與其中已經開始的工作仍會繼續執行,所以我不能把它寫成完整的帳號停用。既有的 GitLab 憑證與代理也不會因此撤銷;收終端或流量畫面的連線若失敗,系統只能回報部分成功。收線本身又是一次快照,在帳號狀態失效與終端真正建立之間,仍有一個很窄的併發窗口。它處理的是小團隊裡「把某個人請出去」,不是即時圍堵正在攻擊的內部人。要做到後者,還需要獨立的帳號狀態、在真正建立終端前重新檢查,以及能持續重試的對帳流程。

這一段我需要自己想嗎?

身分辨認落到實作,第一件事就是怎麼存密碼。對外之後就有登入頁,有登入頁就得存密碼。而存密碼這一題,第一個該問的是:這需要我自己想嗎?

不需要。密碼用 Argon2id,argon2-cffi 的預設就是它,參數用套件的建議值。不明文、不用 sha256、不自己刻。

這個選擇有兩份文件可以指:

  • RFC 9106(IRTF,2021 年 9 月)第 7.4 節直接寫 Argon2id 是 FIRST RECOMMENDED,並建議把它當成所有環境的預設值
  • OWASP 的密碼儲存指引把 Argon2id 列為第一選擇,並給出最低參數:記憶體 19 MiB、迭代 2 次、平行度 1

至於 NIST,狀況值得一提。SP 800-63B 到我寫這篇為止是 Rev 4(2025 年 7 月定案),它講密碼儲存的那一節並沒有點名任何演算法,它要求用「approved」的雜湊方案並指回另一份文件,另外規定鹽至少 32 bits、成本參數要在不拖垮驗證效能的前提下盡可能高,並隨算力成長往上調。

所以我不能拿 NIST 這份文件替 Argon2id 的演算法選擇背書;這裡的選擇依據是 RFC 9106、OWASP 與 argon2-cffi。NIST 支持的是鹽、成本參數,以及日後隨硬體能力調高成本等要求。這不代表選 Argon2id 有問題,而是要把不同文件實際替哪個判斷提供依據說清楚。

回應快一點慢一點,也是答案

身分辨認還會從另一條路洩漏答案,只是這次洩漏的不是回應內容,而是時間。上一節是照著現行建議做就好。這一節不是。

查無此人的時候,程式不會直接回「沒有這個帳號」,而是拿一個預先備好的假雜湊,把送進來的那個密碼照樣驗一次,讓兩條路徑花掉的時間差不多。

防的是時序攻擊。帳號不存在時早早就回來、帳號存在但密碼錯時慢了幾十毫秒,那個時間差本身就是答案:拿一份名單掃過去,光看誰回得慢,就知道哪些帳號是真的。這在 CWE 上有編號,CWE-208,Observable Timing Discrepancy。洩漏資訊的不是邏輯,是實作跑起來的物理特徵。你的程式邏輯完全正確,它還是把答案講出去了。

而 Argon2 選 id 的理由,跟上面不是同一個攻擊面。假雜湊處理的是遠端可觀察的回應時間差,避免有人藉此枚舉帳號;Argon2id 則混合資料無關與資料相關的記憶體存取方式,在旁通道防護與密碼破解成本之間取平衡。兩者都在避免實作特徵成為攻擊線索,但發生的層次不同。

它還有一個兄弟,就是前面「拿別人的 Session 編號」與「拿不存在的編號」都回同一句查無此物。兩個掛在同一個父類底下:CWE-203,Observable Discrepancy,定義是「產品在不同情況下的行為或回應不同,而那個不同被不該看到的人觀察得到」。一個洩漏走內容,一個洩漏走時間,要防的是同一件事:不要讓「不一樣」本身變成答案。

這種東西不會有人來跟你回報,因為畫面上什麼都沒有壞。

兩個看起來一模一樣的帳號

登入流程認得出人還不夠,系統與管理員也得可靠地認出帳號。但這裡有一個我原本沒想到的問題。

使用者名稱如果混進看不見的字元,會發生什麼事?

有一類 Unicode 字元,isprintable() 回 True、isspace() 也回 False,所以一般的檢查全部穿得過去。但畫出來是空的。於是 admin 跟後面多了一個隱形字元的 admin,在清單上、在下拉選單裡、在稽核紀錄上,肉眼完全相同。

所以呢?這裡沒有自助註冊,帳號只有管理員開得了,也沒有改名這個功能,所以不會有外人跑來註冊一個假的 admin。真正會踩到的是另一條:名字常常是貼進來的,從工單、從聊天室、從一份交接的表格。那些來源帶得動零寬字元,也帶得動會改變顯示順序的方向控制碼。

而那份清單就是決定的依據。要停掉哪一個帳號、稽核紀錄上這件事是誰做的,都是照著它讀出來的。上面一旦出現兩列長得一模一樣的名字,這些判斷就沒有依據了。更糟的是等到那時候才發現,已經沒有安全的修法,因為你不知道該動哪一個。

擋在建立的那一刻只要一段判斷。這一題便宜的解法只存在於現在。

第一版我列了五個這種字元擋掉。然後做了一輪對抗性測試,當場又找出七個穿得過去的。

問題不在於漏了七個,在於黑名單這個做法本身。它列的是實例,不是規則,所以永遠有下一個。

與其繼續補黑名單,我去找 Unicode 已經定義好的集合。它有一個屬性叫 Default_Ignorable_Code_Point,指的是除非實作明確支援,否則預設應在繪製時忽略的碼位;Unicode 的識別字 profile 也會排除這一類碼位。前一輪穿過黑名單的七個案例,都落在這個集合裡。

但它不是所有「看起來一樣」問題的完整解法。目前實作還會先用 NFKC 與 casefold 做唯一性比較,並另外擋掉不屬於這個屬性、畫面上卻仍是一格空白的 U+2800。Default_Ignorable_Code_Point 解決的是這次預設可忽略碼位的威脅,不是全部 Unicode 同形字問題。

Python 標準庫沒有開放這個屬性,所以目前依 Unicode 15.1 的碼位區間直接寫進程式裡;日後升級所採用的 Unicode 版本時,這份區間也要一起檢查。

寫完之後,登入頁又漏了一種答案

最後還有一種答案,發生在前面三個問題之前:還沒登入的人,究竟能看見什麼。

這篇寫完後,我把原本由 Jinja 模板與手寫 JavaScript 組成的前端改成 Vue 3。原本直接注入模板的資料也跟著搬到兩條 bootstrap API;就在這次搬動裡,我才發現公開的那一條,會在登入前回傳程式版本與宿主機上的持久化目錄。

它們不是 token,也不能直接拿來登入。但對還沒有通過驗證的人,這兩個值沒有任何完成登入所需的用途;版本可以用來對照已知弱點,絕對路徑則會透露主機上的部署形狀。把它們放在公開回應裡,等於替門外的人先做了一部分盤點。

修法不是只把登入頁的頁尾藏起來。那樣用 HTTP client 仍然拿得到。也不能只收 API 而讓畫面照舊索取,否則留下的是錯誤或一塊空白。最後這兩個值搬到登入後才拿得到的 account bootstrap,登入頁也同一刀不再畫頁尾。

這次補的是「誰能看見什麼」,不是「誰能呼叫什麼」。授權邊界如果只存在畫面上,就不是邊界;API 與 UI 必須一起收,才不會一邊真的公開、一邊只是看起來沒有。

本日小結

接上網路之後多出來的東西,其實不是「加一個登入頁」那麼簡單:

今天真正被授權的,不只是一張頁面,而是一場會持續執行的 Agent:登入決定誰能進來,Session ownership 決定誰能接手,撤銷則決定權限失效時究竟能收回到哪一層。

  1. 授權要掛在資料交出去之前那一刻,因為交出去之後就沒有人會再問了
  2. 有些防護壞掉的時候不會紅,抽屜空白、console 一行錯誤、測試全綠
  3. 撤銷要撤到連線層,改密碼只讓 cookie 失效,那條已經連上的線得主動去切
  4. 黑名單擋不住看不見的字元,要去找標準怎麼定義這件事,列實例永遠有下一個
  5. 授權邊界不能只存在畫面上,API 與 UI 要一起收,否則只是把答案藏起來,沒有停止對外回答

而一開始那條「隔離不等於我碰不到」,我覺得比這五條都重要。上面五件全部做到,它還是成立。做安全的東西很容易做到一個程度就開始覺得自己隔乾淨了,但有些東西不是隔得掉的,只能寫在文件講邊界的那一節,讓人自己判斷要不要進來。

登入回答的是「你是誰」;角色與擁有權一起回答「你能碰哪一場」。

而隔離只能說一般使用者之間沒有混用,不能把應用程式管理員與部署營運者從信任鏈裡變不見。

明天講那個終端到底是什麼東西、我為什麼為了它去改別人的專案,以及改完之後怎麼確認自己沒把它改壞。


上一篇
Day 24|把 Claude Code 搬上瀏覽器:一場 Session 一顆容器
下一篇
Day 26|AI 用 Rust 重寫 ttyd:怎麼證明它沒改壞?
系列文
AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言