一行條件,過一道帶成本表的閘門,只出來一個 true 右邊幾張寫著表示式的卡片排隊走向一道閘門,閘門上方是一個成本錶盤,指標停在低位;穿過閘門後只剩一個打勾的圓形,表示結果是 true。左邊寫著 true、cost 4783 和 has(self.spec)。 true cost 4783 has(self.spec)

Kubernetes 的 CRD 裡經常能看到這樣一段:

YAML
x-kubernetes-validations:
- rule: "self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas"
message: "replicas 必须在 minReplicas 和 maxReplicas 之间"

rule 後面那行字串不是 Go,也不是 JavaScript,apiserver 卻能在每次建立、更新物件時把它算出來。同樣長相的表示式還出現在 Google Cloud 的 IAM 條件(request.time < timestamp("2026-12-31T00:00:00Z"))、Envoy 的 RBAC 過濾器、buf 的 protovalidate 裡1。它叫 CEL,Common Expression Language。

第一次見到它,自然會有幾個疑問:為了一行條件判斷,專門造一門語言值得嗎?直接嵌一個 Lua 或 JavaScript 不行嗎?Go 裡已經有 expr、govaluate 這些表示式庫,策略領域還有 OPA 的 Rego,CEL 和它們比到底差在哪、強在哪?真要在自己的服務裡用,又該怎麼用?

我們從一個很普通的需求出發,看看一路會碰到什麼。

一、把規則交給使用者寫

現象:規則寫死在程式碼裡

假設我們在做一個運維平臺,釋出前要做一次放行判斷。最早的版本直接寫在 Go 裡:

Go
func allow(u User, r Resource, hour int) bool {
return u.Role == "admin" ||
(r.Owner == u.ID && hour >= 9 && hour < 18) ||
slices.Contains(u.Groups, r.Team)
}

管理員直接放行;服務負責人只能在工作時間釋出;服務所屬團隊的成員也可以釋出。很快各個業務線都來提要求:有的要加“週五下午不釋出”,有的要“灰度環境不限時間”。每改一次都要發一次平臺,規則和程式碼綁在一起,顯然走不遠。

第一次嘗試:把規則存成配置

最直接的想法是把規則變成資料。如果用 JSON 描述,社群裡有現成的格式 JsonLogic2:

JSON
{"or": [
{"==": [{"var": "user.role"}, "admin"]},
{"and": [{"==": [{"var": "resource.owner"}, {"var": "user.id"}]},
{">=": [{"var": "hour"}, 9]}, {"<": [{"var": "hour"}, 18]}]},
{"in": [{"var": "resource.team"}, {"var": "user.groups"}]}
]}

它能工作,前端也好做成拖拽編輯器,但人很難直接讀寫。規則一複雜,大家還是希望寫成一行程式碼那樣的東西。

第二次嘗試:嵌入一門指令碼語言

於是很多平臺走向了下一步:嵌一門現成的指令碼語言。Java 世界裡最順手的是 Spring 自帶的 SpEL,Go 世界裡有 gopher-lua、goja(JavaScript)、Starlark。規則立刻好寫了,但新的問題接著出現。我們在同一臺機器上試了三條“使用者寫的規則”:

嵌入指令碼語言後,三條規則在平臺程序裡的結果 左邊是使用者填寫的三條規則:SpEL 的 T(java.lang.System).getProperty('user.name');Lua 的 while true do end;Lua 的 user.rol == "admin"。右邊是平臺程序裡的結果:第一條返回 "root",讀到了宿主 JVM 的系統屬性;第二條 8 秒內沒有返回;第三條欄位寫錯,返回 false 且沒有任何報錯,管理員也被拒絕。 使用者在平臺裡填寫的規則 平臺程序裡求值的結果 SpEL 7.0.9 能呼叫什麼 T(java.lang.System).getProperty('user.name') Lua(gopher-lua 1.1.2) 會不會停 while true do end Lua(gopher-lua 1.1.2) 寫錯了何時知道 user.rol == "admin" or … 返回值 宿主 JVM 的使用者名稱 "root" 返回值 最後被 timeout 殺掉 8 秒內沒有返回 返回值(管理員請求) 沒有任何報錯 false 三條規則都能順利儲存,問題全部出現在求值的時候。 後兩條的問題,平臺程序自己察覺不到:一條佔著執行緒不放,一條悄悄給出錯誤答案。
圖 1 · 嵌入一門完整的指令碼語言,規則能做的事遠遠超出了“判斷真假”

第一條最危險。規則本來只該看到 user、resource、hour 三個變數,SpEL 卻允許用 T(...) 引用任意 Java 類,讀系統屬性只是演示,換成 Runtime.exec 就是遠端執行命令。這不是假想:2017 年 Equifax 的資料洩露,入口是 Struts 把 HTTP 頭裡的一段 OGNL 表示式拿去求值3;2022 年 Spring Cloud Function 的 CVE-2022-22963,是把請求頭裡的 SpEL 拿去求值4。表示式語言能力太強,本身就是攻擊面。

第二條說明,只要語言裡有迴圈,使用者就可能寫出停不下來的程式。第三條說明,指令碼語言通常到執行時才去找欄位,找不到就給一個 nil,nil == "admin" 是 false,規則安靜地給出了錯誤答案。

還有一個問題圖裡沒畫:放行判斷在釋出路徑上,閘道器、准入控制這類場景更是每個請求都要跑,求值本身必須足夠便宜。

問題歸納

把這幾條放在一起,我們需要的其實是這樣一種東西:

  1. 只能讀宿主給它的資料、呼叫宿主給它的函式,別的什麼都碰不到;
  2. 一定會停,而且最好能事先知道最壞要算多久;
  3. 寫錯欄位、型別對不上,在儲存規則時就報錯;
  4. 每次求值在微秒以內。

這幾條單獨看都不難,難的是同時做到。CEL 就是為這個組合設計的。

二、CEL 的出現

來歷

CEL 的規範倉庫 cel-spec 第一個提交是 2017 年 8 月 14 日,提交說明是 “Initial import from go/api-expr”,也就是從 Google 內部的一個表示式專案匯出來的5。README 裡寫得很直白:最初的規範基於開發 Firebase Rules 的經驗,又做了很多輪可用性測試。Firebase 的安全規則正是“讓使用者寫規則、由服務端在每個請求上求值”的典型場景,上面那幾個問題 Google 早就踩過。

此後 CEL 成了 Google 內外很多系統共用的條件語言。2024 年 6 月,Google 在開源部落格上專門介紹了它,概括為快、可移植、可擴充套件、安全四點6;官方站點 cel.dev 的說法是“非圖靈完備,只能訪問宿主程式提供的資料”7。

CEL 的時間線和採用者 左邊是時間線:2017-08 cel-spec 首次提交,從內部 go/api-expr 匯入;2018-03 cel-go 公開,隨後有 C++、Java 實現;2021-12 Kubernetes 1.23 引入 CRD 校驗規則 alpha;2023-12 Kubernetes 1.29 CRD 校驗規則 GA;2024-04 Kubernetes 1.30 ValidatingAdmissionPolicy GA;2024-06 Google 開源部落格專文介紹 CEL;2026 年 Kubernetes 1.36 MutatingAdmissionPolicy GA;2026-06 倉庫遷到 cel-expr 組織,Go 模組路徑 cel.dev/cel-go。右邊列出其他採用者:Google Cloud IAM Conditions、Firebase Security Rules、Cloud 的 Secure Web Proxy、Envoy RBAC 過濾器、buf protovalidate、Kyverno 和 Gatekeeper 的 CEL 策略。 時間線 其他採用者(不分先後) 2017-08cel-spec 首次提交:從 go/api-expr 匯入 2018-03cel-go 公開,隨後有 C++、Java 實現 2021-12K8s 1.23:CRD 校驗規則(alpha) 2023-12K8s 1.29:CRD 校驗規則 GA 2024-04K8s 1.30:ValidatingAdmissionPolicy GA 2024-06Google 開源部落格專文介紹 CEL 2026K8s 1.36:MutatingAdmissionPolicy GA 2026-06倉庫遷到 cel-expr,模組 cel.dev/cel-go Google Cloud IAM Conditions Firebase Security Rules Secure Web Proxy、CA Service Envoy RBAC 過濾器 buf protovalidate Kyverno、Gatekeeper (後兩者都支援用 CEL 寫策略) Kubernetes 一路把 CEL 從 CRD 校驗用到了准入控制:寫進 API 物件、由 apiserver 求值的規則越來越多。
圖 2 · CEL 從 Google 內部的條件語言,長成了雲原生裡寫“判斷”的通用格式

Kubernetes 是最典型的例子。CRD 校驗規則(KEP-2876)在 1.23 進入 alpha,1.29 GA8;用 CEL 寫准入策略的 ValidatingAdmissionPolicy 在 1.30 GA9,能改物件的 MutatingAdmissionPolicy 在 1.36 GA10。在這之前,想在准入階段做點校驗,就得自己寫一個 webhook 服務、部署、配證書,再讓 apiserver 每個請求都遠端呼叫它。CEL 讓這類規則變成 API 物件上的一個欄位,由 apiserver 在程序內直接求值11。

還有兩個跟 Go 使用者直接相關的變化:2026 年 6 月倉庫從 google 遷到了 cel-expr 組織;Go 模組路徑改成了 cel.dev/cel-go,v0.32.0 起再寫 go get github.com/google/cel-go@latest 會報模組路徑不匹配12。

設計上舍棄了什麼

CEL 的規範在開頭就給自己下了定義:記憶體安全、無副作用、一定會終止、強型別,型別檢查可選但推薦13。落實到語法上,就是一系列“沒有”:

  • 沒有迴圈,沒有賦值,沒有函式定義。 一個 CEL 程式只是一個表示式。想遍歷列表,只能用幾個內建宏:all、exists、exists_one、map、filter,它們遍歷的物件一定是已經存在的、有限的列表或 map。
  • 沒有 I/O,也沒有“引用任意型別”的語法。 表示式能用的變數和函式,全部由宿主程式在環境(Env)裡宣告。
  • 沒有異常處理。 執行時錯誤是一個值,只能被邏輯運算子吸收,不能被捕獲(這一點後面會專門講)。

保留下來的,是一套和 C、Go、Java、JavaScript 幾乎一樣的語法,以及對 protobuf 和 JSON 型別的一等支援。所以我們開頭那條規則,用 CEL 寫出來就是:

JavaScript
user.role == "admin" || (resource.owner == user.id && hour >= 9 && hour < 18) || resource.team in user.groups

捨棄了這些,換來了什麼?下面兩節分別看“寫錯了何時知道”和“會不會停”。

三、解析、檢查、求值

三個階段

在 Go 裡用 CEL,固定是三步:宣告環境、編譯(解析加型別檢查)、生成 Program 後求值。

Go
env, _ := cel.NewEnv(
ext.NativeTypes(reflect.TypeOf(User{}), reflect.TypeOf(Resource{}), ext.ParseStructTags(true)),
cel.Variable("user", cel.ObjectType("main.User")),
cel.Variable("resource", cel.ObjectType("main.Resource")),
cel.Variable("hour", cel.IntType),
)
ast, iss := env.Compile(src) // 解析 + 类型检查
prg, _ := env.Program(ast) // 生成可复用、并发安全的 Program
out, _, _ := prg.Eval(map[string]any{"user": u, "resource": r, "hour": 23})
CEL 的三個階段和各自的耗時 左邊“儲存規則時”區域有兩張卡片。cel.NewEnv 宣告 user 為 main.User、resource 為 main.Resource、hour 為 int。env.Compile 解析並檢查表示式:正確的規則輸出型別為 bool;把 role 寫成 rol 時報 undefined field 'rol';耗時約 100 微秒。右邊“每次請求”區域:快取的 Program 接收 user 和 hour 等輸入,返回 true,耗時約 0.47 微秒。 儲存規則時(每條規則一次) 每次請求(成千上萬次) cel.NewEnv 宣告 usermain.User resourcemain.Resource hourint 變數和函式只能來自這裡 env.Compile 解析 + 檢查 user.role == "admin" || … → 輸出型別 bool user.rol == "admin" → undefined field 'rol' 約 100 µs prg.Eval 快取的 Program user{role: "dev", …} hour23 → true 約 0.47 µs 檢查和求值分開:貴的那一步(編譯)在儲存規則時做一次,便宜的那一步(求值)在每個請求上做。 寫錯的欄位在儲存時就被攔下,根本到不了請求路徑。
圖 3 · 編譯比求值貴 200 倍,所以 CEL 把它們拆成兩個階段

這個拆分有兩層意思。第一層是效能:我們測得每次都重新編譯約 100 µs,快取 Program 後只求值約 0.47 µs,差了 200 多倍。第二層是檢查:因為環境裡宣告瞭 user 是 main.User,檢查器知道它有哪些欄位,user.rol 在編譯時就報錯,hour >= "9" 也會報 found no matching overload for '_>=_' applied to '(int, string)'。

第一次嘗試:變數宣告成 map

很多人第一次用 CEL,會把輸入宣告成 map(string, dyn),省得定義型別。這樣做也能跑,但檢查器就不知道 map 裡有哪些鍵了。user.rol 能透過編譯,到執行時才報 no such key: rol。

而且這個錯誤不一定能看到。我們給一個 9 點到 18 點之間、自己名下的資源發請求:

user.rol == "admin" || hour < 18 => true
hour < 18 || user.rol == "admin" => true
user.rol == "admin" || hour > 18 => error: no such key: rol
user.rol == "admin" ? true : hour < 18 => error: no such key: rol
has(user.rol) && user.rol == "admin" || hour < 18 => true

原因:邏輯運算子會吸收錯誤

這是 CEL 和大多數語言不一樣的地方。在 C、Go 裡,a || b 從左往右求值,a 出錯就整個出錯。CEL 規範規定 && 和 || 是可交換的:只要有一邊已經能決定結果(|| 的一邊是 true,&& 的一邊是 false),另一邊的錯誤就被忽略,跟左右順序無關13。規範給的理由是,這樣邏輯表示式可以對映到索引查詢,和 SQL 的語義一致。想要傳統的短路,就改寫成三元表示式 a ? true : b。

所以 map 宣告加上錯誤吸收,欄位寫錯的規則可能在大部分請求上“看起來沒問題”,只在某些分支上報錯。解決辦法是把變數宣告成具體型別:用 ext.NativeTypes 註冊 Go 結構體,或者直接用 protobuf 訊息型別。確實需要處理“可能不存在的欄位”時,用 has(user.nickname) 或可選值語法 user.?nickname.orValue("") 明確寫出來,不要指望錯誤吸收替你兜底。

型別檢查解決了“寫錯了何時知道”。那“會不會停”呢?沒有迴圈,是不是就萬事大吉?

四、會停,但不一定停得快

現象:一條“合法”的規則算了 12 秒

CEL 沒有迴圈,但宏可以巢狀。一個 10 個元素的列表,巢狀 8 層 all:

JavaScript
[0,1,2,3,4,5,6,7,8,9].all(a, [0,1,2,3,4,5,6,7,8,9].all(b, … .all(h, true)))

這條表示式只有兩百多個字元,卻要迭代 10810^8 次。我們不加任何限制直接跑,cel-go 用了 12.6 秒才返回 true。它確實會停,規範沒有騙人,但對一個每個請求都要算的規則來說,12 秒和不停沒有區別。規範自己也承認:除了 has() 以外的宏,是 CEL 裡唯一可能出現指數代價的地方13。

原因與解決:把“多久”變成一個數

非圖靈完備的真正價值,不是“一定會停”,而是代價可以事先算出來。因為沒有迴圈和遞迴,每個宏的迭代次數都受輸入列表大小約束,檢查器可以沿著語法樹估算出最壞的代價。CEL 用一個抽象的“成本單位”來計數,比較運算算 1,列表字面量算 40,正則匹配按正則長度乘輸入長度估算11。

於是有了兩道閘門:

CEL 的兩道成本閘門 左邊是寫入時的靜態估算:表示式 containers.all(c, c.image.matches(re)),不宣告列表和字串的最大長度時,估算最大成本為 18446744073709551615,超過 Kubernetes 單條上限 1,000,000,會被拒絕寫入;宣告 maxItems 20、maxLength 256 後最大成本為 4783,允許寫入。右邊是求值時的執行時計數:8 層巢狀 all,10 的 8 次方次迭代,估算最大成本 455,555,551;不設限時 12.6 秒後返回 true;設定 CostLimit 1e6 後 97 毫秒中止,報 actual cost limit exceeded。 寫入時:靜態估算 求值時:執行時計數 containers.all(c, c.image.matches(re)) 不宣告最大長度 max = 18446744073709551615 拒絕 maxItems: 20 · maxLength: 256 max = 4783 允許 K8s 單條上限 PerCallLimit = 1,000,000 8 層巢狀 all,10⁸ 次迭代 靜態估算 max = 455,555,551 不設限 12.6 s 後返回 true cel.CostLimit(1_000_000) 97 ms 中止:cost limit exceeded 靜態估算擋住“可能很貴”的規則,執行時計數擋住“真的很貴”的那一次求值。 列表和字串沒有上限,估算就只能給出 2⁶⁴ − 1,所以 K8s 要求在 schema 裡寫 maxItems、maxLength。
圖 4 · 非圖靈完備的真正好處:最壞代價可以在寫入前算出來,求值時也能逐步計數

執行時計數很直觀:求值器每執行一步就累加成本,超過 cel.CostLimit 就中止。上面那條 10810^8 的表示式,設了 100 萬的上限後 97 毫秒就停了,錯誤是 operation cancelled: actual cost limit exceeded。再配合 cel.InterruptCheckFrequency 和 ContextEval,宏每迭代一定次數就檢查一次 context,超時也能及時退出。

靜態估算更有意思。env.EstimateCost 不需要任何輸入資料,只看語法樹和型別宣告。問題在於,對一個長度未知的列表,最壞情況就是無窮大:同一條檢查映象地址的規則,不告訴估算器列表多長、字串多長,估出來是 264−12^{64}-1;宣告最多 20 個容器、映象名最長 256 個字元,估算立刻降到 4783。

Kubernetes 把這兩道閘門都用上了。apiserver 的常量裡寫著:單條表示式 PerCallLimit = 1000000,註釋說大約 0.1 秒;每個自定義資源或每個 ValidatingAdmissionPolicy binding 的總預算 RuntimeCELCostBudget = 10000000,大約 1 秒14。寫 CRD 時,如果某條規則的估算成本超過上限,apiserver 直接拒絕這個 CRD,報錯會提示你給相關欄位加上 maxItems、maxLength。這就是為什麼寫 CRD 校驗規則時,schema 裡那些長度限制不是可有可無的。

到這裡,CEL 對開頭四個問題都給出了答案。但 Go 生態裡做表示式和規則的庫遠不止 CEL 一個,它們各自是怎麼回答這幾個問題的?

五、和同類語言對比

參加對比的語言

我們把能嵌程序序裡寫“判斷”的東西儘量列全,按作者主要用的 Go 生態排主次,Java 生態單獨一組。凡是有 Go 實現的都裝進同一個程式裡,用同一條放行規則、同一組輸入實測;Java 的幾個用 JDK 21 單獨測。

參加對比的 22 個實現
語言出處 · 實測版本定位能不能寫迴圈
表示式語言(一個表示式,算出一個值)
CELGoogle,2017;cel-go v0.32.0,另有 C++、Java通用條件語言只有 all / exists / map / filter 宏
exprexpr-lang;v1.17.8Go 表示式語言內建 all / map / filter,有區間 1..n
govaluateKnetic;v3.0.0,已多年不更新Go 表示式不能
gvalPaesslerAG;v1.2.4可擴充套件的 Go 表示式不能
JsonLogicJeremy Wadhams;Go 移植 v3.10.1規則即 JSONall / map / reduce
JMESPathAWS CLI 的查詢語言;go-jmespath v0.4.0JSON 查詢只能投影輸入
jqStephen Dolan,2012;gojq v0.12.19JSON 處理能(遞迴)
策略語言
RegoOPA,CNCF 畢業專案;v1.21.3Datalog 風格的策略語言推導式,禁止遞迴
CedarAWS,2023 年開源;cedar-go v1.8.0授權策略不能
配置語言
CUEMarcel van Lohuizen;v0.17.1配置 + 校驗推導式
JsonnetGoogle,2014;go-jsonnet配置生成能(遞迴)
嵌入式指令碼
StarlarkBazel 的 Python 方言;starlark-go確定性指令碼for 能;while、遞迴預設禁止
Luagopher-lua v1.1.2通用指令碼能
JavaScriptgoja通用指令碼能
Tengod5/tengo v2.17.0Go 原生指令碼能
Java 生態(單獨實測)
SpELSpring 7.0.9Spring 的表示式語言不能,但能呼叫任意方法
MVEL2.6.0.Final表示式 + 指令碼能
Aviator5.4.4表示式 + 指令碼能
JEXLApache Commons 3.7.1表示式 + 指令碼能
CEL-Javadev.cel 0.14.0CEL 的 Java 實現同 CEL

expr 和 cel-go 各測了兩種輸入:map[string]any 和 Go 結構體,所以 Go 側一共 17 個配置。沒有實測的:HashiCorp Sentinel(商業產品)、Apple 的 Pkl、Dhall、Go 直譯器 Yaegi、OGNL(它的問題見第一節的 Struts 漏洞)。

表 1 · 同類工具:從只能寫一個表示式,到完整的指令碼語言

場景一:同一條規則怎麼寫

17 個 Go 配置都寫出了開頭那條規則,6 個測試用例(管理員、工作時間內的負責人、下班後的負責人、團隊成員、無關的人、沒有任何組的人)全部透過。寫法上的差別,比想象中能說明問題:

JavaScript
// CEL、expr、gval:三者完全一样
user.role == "admin" || (resource.owner == user.id && hour >= 9 && hour < 18) || resource.team in user.groups
# Rego:每条 allow 是一条独立规则,命中任意一条即为真
package authz
default allow := false
allow if input.user.role == "admin"
allow if {
input.resource.owner == input.user.id
input.hour >= 9
input.hour < 18
}
allow if input.resource.team in input.user.groups
// Cedar:先建实体模型,团队关系用 in 表达成实体层级
permit (principal, action == Action::"deploy", resource)
when {
principal.role == "admin" ||
(resource.owner == principal && context.hour >= 9 && context.hour < 18) ||
principal in resource.team
};

CEL 和 expr 一模一樣,因為它們都選了 C 系語法;Rego 把“或”拆成多條規則,讀起來像邏輯程式;Cedar 要求先把使用者、團隊、服務建模成實體,規則裡的 principal in resource.team 是沿著實體層級判斷的。Lua、Tengo 沒有 in,要自己寫一個 contains 迴圈;JsonLogic 和 JMESPath 只認 JSON 的 float64,傳 Go 的 int 會報錯或比較失敗。

場景二:寫錯欄位、寫錯型別

接下來把每個引擎裡的 user.role 改成 user.rol,再把 hour >= 9 改成 hour >= "9",看誰在什麼時候發現:

欄位寫錯、型別寫錯,什麼時候發現
引擎user.rol(欄位寫錯)hour >= "9"(型別寫錯)
CEL(Go 結構體)編譯期:undefined field 'rol'編譯期:no matching overload for '_>=_' applied to '(int, string)'
CEL(map / dyn)執行時:no such key;另一邊為 true 時被吸收
expr(Go 結構體)編譯期:type main.User has no field Rol編譯期:mismatched types int and string
expr(map)靜默返回 false
Rego + JSON Schema編譯期:undefined ref: input.user.rol靜默返回 false
Rego(無 schema)靜默返回 false靜默返回 false
Cedar執行時:策略報錯,不生效執行時:type error
govaluate、Starlark、CUE、Jsonnet執行時報錯執行時報錯,只在走到這一支時
Lua、Tengo靜默返回 false執行時報錯,只在走到這一支時
gval、JsonLogic、JavaScript靜默返回 false靜默轉換,10 >= "9" 為 true
jq、JMESPath靜默返回 false靜默返回 false

“執行時報錯,只在走到這一支時”:管理員的請求在第一個 || 就決定了結果,這些引擎對它返回 true,不報錯;只有走到 hour 比較的請求才報錯。

表 2 · 能在儲存規則時就發現錯誤的,只有宣告瞭型別的 CEL、expr 和加了 schema 的 Rego

這張表裡最該注意的是紅色那一片:一半的引擎在欄位寫錯時靜默返回 false。對放行規則來說,這意味著管理員突然發不了版,而日誌裡什麼都沒有。能在編譯期攔住兩類錯誤的,只有宣告瞭具體型別的 CEL 和 expr。Rego 加上 JSON Schema 能發現欄位錯誤,但它的比較運算對不同型別有一個全序(數字總是小於字串),所以 10 >= "9" 是合法的、結果是 false。

場景三:寫一個停不下來的表示式

再給每個引擎寫一個“它允許寫出來的最壞表示式”:有迴圈的寫死迴圈,沒有迴圈的寫巢狀迭代。先不加限制跑,外面用 timeout 8s 兜底;再開啟該引擎自帶的限制手段跑一次,上限都設為約 1 秒或 100 萬步。

失控表示式:不設限會怎樣,自帶的手段能不能停住
引擎最壞寫法不設限自帶的限制
CEL8 層巢狀 all,10⁸ 次12.6 s 後返回事先估算 4.6 億;CostLimit 97 ms 中止
expr同上,用區間 0..9108 ms 中止:預設記憶體預算 memory budget exceeded
expr同上,用字面量列表31.7 s沒有 CPU 或步數限制
JsonLogic8 層巢狀 all15.4 s沒有
Starlarkfor i in range(10**9)40.4 sSetMaxExecutionSteps 45 ms 中止
Rego兩層 numbers.range(1, 10000) 推導式8 s 未返回context 超時,1 s 中止
Luawhile true do end8 s 未返回SetContext 超時,1 s 中止
JavaScriptfor (;;) {}8 s 未返回Interrupt,1 s 中止
Tengofor {}8 s 未返回RunContext,1 s 中止
jqdef f: f; f8 s 未返回RunWithContext,1 s 中止
CUE兩層 list.Range(0, 10000, 1) 推導式8 s 未返回沒有
Jsonnetstd.foldl 遍歷 10⁸ 個元素8 s 未返回沒有,只有棧深度限制
govaluate、gval、JMESPath、Cedar寫不出迴圈或巢狀迭代,代價只和表示式長度、輸入大小成正比
表 3 · 大部分引擎都能停住,但只有 CEL 能在執行之前就說出最壞要算多久

能停住的引擎其實不少:Lua、JavaScript、Tengo、jq、Rego 都支援 context 或中斷,Starlark 有步數上限15。差別在於它們都是“跑起來再看”,一條規則在儲存時無法判斷它會不會超時,只能上線後靠超時兜底。CEL 是這裡唯一能在寫入時就給出最壞代價的;expr 有記憶體預算,但用字面量列表就能繞開,31.7 秒沒有任何東西攔它。

場景四:Java 裡的沙箱

Java 生態的表示式語言普遍能直接訪問 Java 類,這正是第一節那幾個漏洞的根源。我們讓每個引擎讀一次系統屬性 user.name,再寫一個死迴圈或阻塞呼叫:

Java 表示式引擎:能不能碰到宿主
引擎讀 System.getProperty("user.name")死迴圈或阻塞
SpEL(StandardEvaluationContext)"root"Thread.sleep:3 s 後仍在跑
SpEL(SimpleEvaluationContext)攔截:Type cannot be found—
MVEL"root"while (true):3 s 後仍在跑
Aviator"root"while true:3 s 後仍在跑
JEXL(預設許可權)攔截:undefined property 'class'while (true):3 s 後仍在跑
JEXL(UNRESTRICTED)"root"—
CEL-Java編譯期:undeclared reference to 'java'寫不出來

SpEL 的 SimpleEvaluationContext 是 Spring 官方給不可信表示式準備的受限模式,代價是方法呼叫也受限:同一條規則裡的 List.contains 就因為找不到方法而報錯。

表 4 · 在 Java 裡,“安全”通常是一個要記得開啟的選項;在 CEL 裡它是語言本身

SpEL 和 JEXL 都提供了受限模式16,問題是它們預設不開,或者一開就要犧牲常用功能。CEL 的安全性不來自配置,而來自語法:語言里根本沒有“引用一個宿主型別”的寫法,java.lang.System 只是一個沒宣告過的識別符號。

場景五:每次求值的耗時

最後是效能。所有引擎都先編譯好、輸入也先轉換成各自需要的格式,只測求值本身(用團隊成員這個用例,前兩個分支都不成立,要一直算到第三個):

同一條規則在 17 個 Go 引擎裡每次求值的耗時 對數座標的橫向條形圖。最快的是 expr 用 Go 結構體,163 納秒;govaluate、JMESPath、Starlark、expr 用 map 在 250 到 450 納秒;CEL 用 map 528 納秒、用 Go 結構體 568 納秒;Cedar 591;goja 1.2 微秒;gval、JsonLogic 1.5 到 2 微秒;jq 3.9 微秒;Lua 6.3 微秒;Rego 10.9 微秒;Tengo 19.5 微秒;CUE 52 微秒;Jsonnet 93 微秒。 每次求值耗時(ns/op,對數座標,已編譯、輸入已備好) 100 ns 1 µs 10 µs 100 µs expr(Go struct) 163 ns govaluate 256 ns JMESPath 284 ns Starlark 335 ns expr(map) 435 ns CEL(map) 528 ns CEL(Go struct) 568 ns Cedar 591 ns JavaScript(goja) 1.2 µs gval 1.5 µs JsonLogic 2.0 µs jq(gojq) 3.9 µs Lua(gopher-lua) 6.3 µs Rego(OPA) 10.9 µs Tengo 19.5 µs CUE 52.0 µs Jsonnet 92.8 µs Lua 的耗時裡包含每次把輸入建成 Lua 表;Rego、CUE、Jsonnet 每次都要走完整的求值管線。
圖 5 · 表示式類引擎都在 1 µs 以內,CEL 約 0.5 µs;策略、配置語言慢一到兩個數量級

表示式類引擎都在 1 µs 以內。expr 用 Go 結構體時最快,163 ns,因為它把欄位訪問編譯成了直接的結構體取值;CEL 在 0.5 µs 左右,map 和結構體兩種輸入差別不大。Rego 約 11 µs,CUE、Jsonnet 每次都要走一遍完整的求值或合一,在 50–100 µs,適合配置生成,不適合放在每個請求上。要注意的是,這裡沒有算上把業務資料轉換成引擎輸入的時間:Cedar 每次要建實體,Jsonnet 每次要序列化成 JSON,加上轉換以後 Cedar 約 3 µs。

場景六:只知道一半輸入的時候

還有一種場景很少有人提,但很實用。釋出列表頁要展示“當前使用者能釋出哪些服務”,如果對每個服務都跑一遍規則,服務一多就慢。更好的辦法是:使用者的資訊已知,服務的資訊未知,讓規則自己“化簡”成一個只和服務有關的條件,再把它變成資料庫查詢。

CEL 的部分求值(partial evaluation)就是幹這個的:

Go
prg, _ := env.Program(ast, cel.EvalOptions(cel.OptTrackState, cel.OptPartialEval))
vars, _ := cel.PartialVars(map[string]any{"user": u, "hour": 10}, cel.AttributePattern("resource"))
_, det, _ := prg.Eval(vars)
residual, _ := env.ResidualAst(ast, det)
s, _ := cel.AstToString(residual)
// u1、dev、组 [web]、10 点:resource.owner == "u1" || resource.team in ["web"]
// 管理员:true

剩下的殘差表示式 resource.owner == "u1" || resource.team in ["web"] 只涉及服務欄位,可以直接翻譯成 WHERE owner = 'u1' OR team IN ('web')。這正是第三節提到的、規範讓 && 和 || 可交換的理由:邏輯表示式要能對映到索引查詢。在所有參加對比的引擎裡,只有 CEL 和 Rego 有這個能力;Rego 的部分求值給出的是 "u1" = input.resource.owner 和 internal.member_2(input.resource.team, ["web"]) 兩條查詢17,OPA 社群也一直用它把策略編譯成 SQL 過濾條件。

對照

把六個場景放在一起:

主要候選在六個場景裡的表現
場景CELexprRegoCedarStarlark / Lua / JS
能碰到什麼只有宣告的變數和函式只有 Env有 http.send 等內建函式,可關只有實體和上下文取決於注入了哪些庫
寫錯欄位編譯期(需宣告型別)編譯期(需傳結構體)需配 JSON Schema執行時;Rust 版可用 schema 校驗執行時或靜默
寫錯型別編譯期編譯期靜默執行時執行時或靜默
失控事先估算 + 執行時計數記憶體預算,可繞開context 超時寫不出迴圈步數或超時
每次求值約 0.5 µs0.16–0.44 µs約 11 µs約 0.6 µs(不含建實體)0.3 / 6.3 / 1.2 µs
部分求值有無有無無
跨語言Go、C++、Java 等,同一規範只有 GoGo,另有 WasmRust、Go各有多種實現
表 5 · CEL 不是每一項都最好,但它是唯一每一項都不差的

結論可以這樣理解:

  • CEL 和 expr18 是最接近的一對。 語法相同,都有型別檢查,都快。expr 更快、函式更多、寫起來更像 Go;CEL 多了成本估算、部分求值和跨語言的同一套規範。規則只在 Go 服務裡、由自己團隊寫,expr 很合適;規則來自使用者、要在寫入時就判斷安全,或者同一條規則要在 Go 後端、Java 服務、前端校驗裡都能跑,選 CEL。
  • Rego 解決的是更大的問題。 它是一個策略引擎:多條規則組合、引用外部資料、統一的策略中心、決策日誌。把它當成“一行條件”的求值器來用,既慢又繞。
  • Cedar19 專注授權。 實體模型和 permit / forbid 結構讓它天然適合回答“誰能對什麼做什麼”,也更容易做形式化分析;但它不是通用的條件語言。
  • Starlark、Lua、JavaScript 是指令碼。 真需要迴圈、區域性變數、多步計算時才用它們,用的時候必須自己加超時、步數限制,並小心控制注入的庫。
  • CUE、Jsonnet 是配置語言,jq、JMESPath 是 JSON 查詢語言。它們能寫判斷,但不是為每個請求求值設計的。

選定了 CEL,在 Go 服務裡怎麼用才能把上面這些好處都拿到?

六、在 Go 裡用好 CEL

前面幾節已經暗示了大部分做法。把它們按規則的生命週期排一下:

一條 CEL 規則從儲存到使用的三個場合 儲存時:env.Compile 做型別檢查,確認 OutputType 是 bool,EstimateCost 不超過一百萬,然後快取 Program。請求時:按規則 ID 取出快取的 Program,ContextEval 帶 50 毫秒超時和成本上限,出錯一律拒絕。列表查詢時:PartialVars 只給 user 和 hour,resource 標為未知,ResidualAst 得到 owner == "u1" 或 team in ["web"],翻譯成 SQL 的 WHERE owner='u1' OR team IN ('web')。 儲存時 請求時 列表查詢時 env.Compile(src) 欄位、型別在這裡檢查 OutputType == bool EstimateCost ≤ 1e6 快取 Program cache[ruleID] Program 併發安全 ContextEval(ctx, vars) 超時 50 ms · CostLimit 1e6 err != nil → 拒絕 PartialVars(user, hour) resource 標為未知 owner == "u1" || team in ["web"] WHERE owner = 'u1' OR team IN ('web') 貴的、會失敗的工作都放在儲存時;請求路徑上只剩一次求值和一個兜底的超時。
圖 6 · 一條規則的三個使用場合,各自該做什麼

下面逐條說明每個做法解決的是什麼問題。

1. 環境宣告具體型別

問題:宣告成 map(string, dyn) 時,欄位寫錯要到執行時才發現,還可能被 || 吸收(第三節)。

做法:輸入用 Go 結構體的話,ext.NativeTypes 加 ext.ParseStructTags(true),欄位名用 cel:"id" 這樣的標籤指定;輸入本來就是 protobuf 的話,cel.Types(&pb.Request{}) 加 cel.Variable("request", cel.ObjectType("pkg.Request")),這也是 CEL 原生的方式。環境在程序啟動時建一次,所有規則共用。

2. 儲存時編譯,並檢查輸出型別

問題:user.role 也是一條合法的 CEL 表示式,結果是字串。放行規則返回了字串,求值時才發現就晚了。

做法:儲存規則時呼叫 env.Compile,把 iss.Err() 原樣返回給使用者(它帶行列號和一個指向出錯位置的 ^);再檢查 ast.OutputType().IsExactType(cel.BoolType)。透過以後把規則原文存起來,程序裡快取 Program。每次請求重新編譯要 100 µs,快取後只剩 0.5 µs。

3. 用尺寸提示估算成本,並設定執行時上限

問題:規則會停,但可能很慢(第四節);而列表長度未知時,估算出的上限沒有意義。

做法:實現一個 checker.CostEstimator,在 EstimateSize 裡按欄位路徑返回業務上的最大長度(比如一個使用者最多 64 個組),儲存時用 env.EstimateCost 拒絕超過上限的規則。生成 Program 時加 cel.CostLimit 和 cel.InterruptCheckFrequency(100),請求時用 ContextEval 帶上超時。我們實測 CostLimit 會讓這條規則的求值從 0.47 µs 變成 1.47 µs,因為每一步都要計數;如果規則全部由內部人員編寫、並且儲存時已經估算過,可以只靠估算和超時,省掉這部分開銷。

4. 出錯一律按拒絕處理

問題:求值出錯時(欄位缺失、超出成本、超時),返回值不是 bool。

做法:放行類規則 fail closed,任何錯誤都當作“不允許”,同時把錯誤記日誌。Kubernetes 的 ValidatingAdmissionPolicy 用 failurePolicy 讓管理員顯式選擇出錯時是放行還是拒絕,也是同一個思路。

5. 自定義函式要是純函式

問題:CEL 的安全性來自“只能呼叫宿主宣告的函式”。如果宿主宣告瞭一個 httpGet(url),前面所有保證都失效了:它有副作用,成本也無法估算。

做法:用 cel.Function 註冊的函式只做純計算(解析版本號、判斷 CIDR、查一個預先載入好的記憶體表),不做 I/O。需要外部資料時,在求值前由宿主準備好放進輸入。函式的代價不是常數的,在 EstimateCallCost 和執行時成本估算裡報上去,否則估算器會按 1 計算。

6. 環境要版本化

問題:規則存進資料庫後,平臺升級 cel-go、開啟新的擴充套件庫,老規則的行為可能變化;反過來,回滾到舊版本時,新語法寫的規則可能無法編譯。

做法:擴充套件庫都帶版本號,例如 ext.Strings(ext.StringsVersion(2)),顯式寫死版本,升級時再統一評估。Kubernetes 就是這樣做的:每個 CEL 庫從哪個版本開始可用寫在文件裡,新功能先只允許已存在的規則繼續求值、不允許新寫入,等到可以安全回滾了再放開11。

7. 需要“列出所有能訪問的物件”時用部分求值

問題:對每個候選物件都跑一遍規則,物件一多就慢。

做法:已知的部分(使用者)放進 PartialVars,未知的部分(物件)用 AttributePattern 標出來,拿到殘差表示式後翻譯成資料庫查詢,像第五節場景六那樣。

在 Kubernetes 裡

在 Kubernetes 裡寫 CEL,上面幾條大多已經由 apiserver 做好了:型別來自 OpenAPI schema,成本估算和執行時預算是內建的。我們要做的主要是兩件事:給 schema 裡的列表和字串加 maxItems、maxLength,讓估算有意義;把重複的子表示式提到 variables 裡,給失敗寫一條有用的 messageExpression。比如要求 Deployment 的映象都來自公司倉庫:

YAML
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: image-from-corp-registry
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
variables:
- name: badImages
expression: >-
object.spec.template.spec.containers
.filter(c, !c.image.startsWith('registry.corp/'))
.map(c, c.image)
validations:
- expression: "size(variables.badImages) == 0"
messageExpression: "'镜像必须来自 registry.corp:' + variables.badImages.join(', ')"

七、回到開頭的放行規則

現在可以把第一節的需求完整地做出來了。下面這段程式就是第六節幾條做法的組合:環境用具體型別,儲存時檢查輸出型別和成本,請求時帶超時和成本上限,出錯就拒絕。

Go
package main
import (
"context"
"errors"
"fmt"
"reflect"
"time"
"cel.dev/cel-go/cel"
"cel.dev/cel-go/checker"
"cel.dev/cel-go/ext"
)
type User struct {
ID string `cel:"id"`
Role string `cel:"role"`
Groups []string `cel:"groups"`
}
type Resource struct {
Owner string `cel:"owner"`
Team string `cel:"team"`
}
const costLimit = 1_000_000
// 一个进程只建一次:声明变量的具体类型,写错字段在保存时就报错。
var env = must(cel.NewEnv(
ext.NativeTypes(reflect.TypeOf(User{}), reflect.TypeOf(Resource{}), ext.ParseStructTags(true)),
cel.Variable("user", cel.ObjectType("main.User")),
cel.Variable("resource", cel.ObjectType("main.Resource")),
cel.Variable("hour", cel.IntType),
))
// 保存规则时调用:检查、限定输出类型、估算成本,返回可缓存的 Program。
func compileRule(src string) (cel.Program, error) {
ast, iss := env.Compile(src)
if iss.Err() != nil {
return nil, iss.Err()
}
if !ast.OutputType().IsExactType(cel.BoolType) {
return nil, fmt.Errorf("规则必须返回 bool,实际是 %v", ast.OutputType())
}
est, err := env.EstimateCost(ast, sizeHints{})
if err != nil {
return nil, err
}
if est.Max > costLimit {
return nil, fmt.Errorf("规则最坏成本 %d 超过上限 %d", est.Max, costLimit)
}
return env.Program(ast, cel.CostLimit(costLimit), cel.InterruptCheckFrequency(100))
}
// 每次请求调用:带超时,任何错误都按拒绝处理。
func allow(ctx context.Context, prg cel.Program, u User, r Resource, hour int) (bool, error) {
ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
defer cancel()
out, _, err := prg.ContextEval(ctx, map[string]any{"user": u, "resource": r, "hour": hour})
if err != nil {
return false, err
}
b, ok := out.Value().(bool)
if !ok {
return false, errors.New("non-bool result")
}
return b, nil
}
// 告诉估算器:groups 最多 64 个,字符串最长 128。
type sizeHints struct{}
func (sizeHints) EstimateSize(n checker.AstNode) *checker.SizeEstimate {
if p := n.Path(); len(p) > 0 {
switch p[len(p)-1] {
case "groups":
return &checker.SizeEstimate{Min: 0, Max: 64}
case "id", "role", "owner", "team", "@items":
return &checker.SizeEstimate{Min: 0, Max: 128}
}
}
return nil
}
func (sizeHints) EstimateCallCost(fn, overload string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate {
return nil
}
func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}

拿開頭那條規則和幾條有問題的規則試一下:

user.role == "admin" || (resource.owner == user.id && hour >= 9 && hour < 18) || resource.team in user.groups
保存成功,求值:true
user.rol == "admin"
拒绝保存:ERROR: <input>:1:5: undefined field 'rol'
hour >= "9"
拒绝保存:ERROR: <input>:1:6: found no matching overload for '_>=_' applied to '(int, string)'
user.role
拒绝保存:规则必须返回 bool,实际是 string
user.groups.all(a, user.groups.all(b, user.groups.all(c, user.groups.all(d, a + b + c + d != ""))))
拒绝保存:规则最坏成本 2081972611 超过上限 1000000

第一節圖 1 裡的三個問題,在這裡都在儲存時被攔下了:沒有辦法引用 java.lang.System 這類宿主之外的東西;巢狀四層遍歷最多 64 個元素的組,估算出 20 億的成本,直接拒絕;欄位寫錯報出了具體的行列號。

小結

  1. 為什麼需要一門專門的表示式語言? 把規則交給使用者寫,嵌入完整的指令碼語言會帶來四個問題:能碰到宿主、可能停不下來、寫錯了要到執行時才知道(甚至靜默給出錯誤答案)、求值不夠便宜。SpEL、OGNL 的幾次遠端程式碼執行漏洞都出在這裡。
  2. CEL 從哪來? 2017 年從 Google 內部的 go/api-expr 匯出,基於 Firebase 安全規則的經驗設計,後來用在 IAM 條件、Envoy、Kubernetes 的 CRD 校驗和准入策略裡。
  3. 它捨棄了什麼、換來了什麼? 沒有迴圈、賦值、I/O 和引用宿主型別的語法,換來了只能訪問宿主給的資料、型別可以在編譯期檢查、最壞代價可以在寫入前估算。
  4. 非圖靈完備就夠了嗎? 不夠。巢狀宏照樣能寫出 12 秒的表示式,要靠靜態估算(配合列表、字串的最大長度)和執行時成本上限兩道閘門。
  5. 和同類比怎麼樣? 22 個實現在同一組場景裡實測:CEL 和 expr 最接近,expr 更快,CEL 多了成本估算、部分求值和跨語言規範;Rego 是策略引擎,Cedar 專注授權,Starlark、Lua、JavaScript 是指令碼,CUE、Jsonnet 是配置語言。CEL 不是每項最好,但每項都不差。
  6. 在 Go 裡怎麼用? 環境宣告具體型別;儲存時編譯、檢查輸出型別、估算成本,快取 Program;請求時帶超時和成本上限,出錯就拒絕;自定義函式保持純函式;擴充套件庫寫死版本;列表查詢用部分求值。

參考資料

參考資料

  1. Advanced CEL topicsBufbuf.build ↗“If you've written expressions for conditional IAM, its Secure Web Proxy, or Firebase security rules, you've used CEL.” protovalidate 用 CEL 寫欄位約束。https://buf.build/docs/protovalidate/cel ↩

  2. JsonLogicjsonlogic.comjsonlogic.com ↗規則本身是 JSON;文中用 Go 移植 diegoholiveira/jsonlogic v3.10.1。https://jsonlogic.com/ ↩

  3. CVE-2017-5638:Apache Struts Jakarta Multipart 解析器遠端程式碼執行NVDnvd.nist.gov ↗攻擊者在 Content-Type 頭裡放一段 OGNL 表示式即可執行任意命令,Equifax 2017 年的資料洩露就是從這裡進去的。https://nvd.nist.gov/vuln/detail/CVE-2017-5638 ↩

  4. CVE-2022-22963:Spring Cloud Function 路由表示式 SpEL 注入NVDnvd.nist.gov ↗spring.cloud.function.routing-expression 請求頭裡的 SpEL 被直接求值,導致遠端程式碼執行。https://nvd.nist.gov/vuln/detail/CVE-2022-22963 ↩

  5. cel-expr/cel-spec:Common Expression Language 規範GitHubgithub.com ↗README 的設計目標:線性時間、無副作用、非圖靈完備;“The initial spec was based on the experience of developing Firebase Rules”。第一個提交是 2017-08-14 的 “Initial import from go/api-expr”。https://github.com/cel-expr/cel-spec ↩

  6. Google、オープンソースの高速で安全な式言語「Common Expression Language」を発表@IT · 2024-06-24atmarkit.itmedia.co.jp ↗2024 年 6 月 Google 開源部落格專文介紹 CEL 的報道:速度、可移植、可擴充套件、安全四個特點,以及 Kubernetes、IAM 中的用途。https://atmarkit.itmedia.co.jp/ait/articles/2406/24/news056.html ↩

  7. cel.devGooglecel.dev ↗官方站點:Speed、Portability、Safety(Non-Turing complete, only accesses data provided by the host application)、Verifiability。https://cel.dev/ ↩

  8. KEP-2876:CRD Validation Expression LanguageGitHubgithub.com ↗alpha 1.23,beta 1.25,stable 1.29。https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/2876-crd-validation-expression-language ↩

  9. KEP-3488:CEL for Admission Control(ValidatingAdmissionPolicy)GitHubgithub.com ↗alpha 1.26,beta 1.28,stable 1.30。https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/3488-cel-admission-control ↩

  10. KEP-3962:Mutating Admission PoliciesGitHubgithub.com ↗alpha 1.32,beta 1.34,stable 1.36。https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/3962-mutating-admission-policies ↩

  11. Common Expression Language in KubernetesKuberneteskubernetes.io ↗Kubernetes 裡 CEL 的用途、各擴充套件庫開始支援的版本、執行時成本預算和寫入時的估算成本檢查。https://kubernetes.io/docs/reference/using-api/cel/ ↩ ↩2 ↩3

  12. cel-expr/cel-goGitHubgithub.com ↗Go 實現,文中用 v0.32.0。2026-06-16 倉庫遷到 cel-expr 組織,模組路徑為 cel.dev/cel-go。https://github.com/cel-expr/cel-go ↩

  13. CEL Language DefinitionGitHubgithub.com ↗memory-safe、side-effect-free、terminating、gradually-typed;&& / || 可交換並吸收錯誤;Performance Limits 一節:除 has() 外的宏是唯一可能出現指數代價的地方。https://github.com/cel-expr/cel-spec/blob/master/doc/langdef.md ↩ ↩2 ↩3

  14. kubernetes:staging/src/k8s.io/apiserver/pkg/apis/cel/config.goGitHubgithub.com ↗PerCallLimit = 1000000(約 0.1 秒)、RuntimeCELCostBudget = 10000000(每個 CR 或 VAP binding,約 1 秒)、CheckFrequency = 100。https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go ↩

  15. google/starlark-goGitHubgithub.com ↗Bazel 配置語言的 Go 實現;預設不允許 while 和遞迴,Thread.SetMaxExecutionSteps 限制執行步數。https://github.com/google/starlark-go ↩

  16. Spring Framework:SpEL Evaluation ContextSpringdocs.spring.io ↗SimpleEvaluationContext 只支援 SpEL 的一個子集,排除了型別引用、構造器和 bean 引用,用於不可信表示式。https://docs.spring.io/spring-framework/reference/core/expressions/evaluation.html ↩

  17. Open Policy Agent:Policy Language(Rego)OPAopenpolicyagent.org ↗文中用 OPA v1.21.3 的 Go SDK。部分求值(Partial Evaluation)和 JSON Schema 型別檢查分別見 docs 的 Partial Evaluation、Schemas 兩節。https://www.openpolicyagent.org/docs/policy-language ↩

  18. expr-lang/exprGitHubgithub.com ↗Go 表示式語言,文中用 v1.17.8。expr.Env 傳結構體時做靜態型別檢查;預設記憶體預算 conf.DefaultMemoryBudget = 1e6。https://github.com/expr-lang/expr ↩

  19. Cedar Policy LanguageAWScedarpolicy.com ↗AWS 2023 年開源的授權策略語言,Amazon Verified Permissions 使用;文中用 cedar-go v1.8.0。https://www.cedarpolicy.com/ ↩

延伸閱讀