vatt'ghern jaskier's ballads
本文 3 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

一個任何人都能填表註冊的公開入口,把一名研究者自動加進了 FIFA 的 Microsoft Entra tenant。他的帳號標著「NO_ROLES」——理論上什麼都不能做。然後他在東京的一台 PC 上,用 VLC 播出了一場進行中的世界盃比賽的戰術攝影機畫面。

我差點 rickroll 整場 FIFA 世界盃

NO_ROLES。署名 BobDaHacker 的研究者在他那篇 6 月 16 日的紀錄裡,反覆回到這個字串。它印在他 JWT 的 claim 裡,是 FIFA 系統發給一個「沒有任何角色」的帳號的標記。前端看到它,就老老實實渲染了一頁 access-denied。問題是,整起事件的全部張力都藏在這頁 access-denied 背後——那扇門看起來鎖著,門框後面卻什麼也沒有。這篇文章按他的時間線重走一遍:他怎麼進去、怎麼確認是真的、衝擊評估到哪裡為止、以及那通凌晨三點的揭露電話接力。每一步都抽得出一條工程教訓,而這些教訓加起來只指向一件事——access control 一旦只活在前端,後端就等於沒有門。

先把可信度的框架交代清楚。這篇是研究者的第一人稱紀錄,署名 BobDaHacker,發表於 6 月 16 日,正值世界盃進行期間;原文沒有給出漏洞窗口的確切日曆日期,只用「Night 0/隔日」描述節奏。所以下文重建的時間線忠於文章的事件順序,但時間間距是示意而非精確刻度。涉及的系統、漏洞類別、存取範圍、揭露對象,下面都會盡量貼著原文的字句走;凡是作者沒講清楚的地方——例如那個自動加入 tenant 的流程細節——這裡會明說是推測,而不是替它補上一個聽起來合理的答案。安全寫作最容易出錯的地方就是這種「替故事補洞」,所以這篇刻意把「作者驗證過的」和「作者只是假設的」分得很開。

一個零權限帳號是怎麼長出來的

起點平淡到不像故事。agents.fifa.org 是 FIFA 的 agent 公開註冊入口,任何人都能填表、過一道身分驗證、收到確認信。作者照做了。他沒注意到的是,這個動作把他的帳號自動加進了 FIFA 的 Microsoft Entra tenant——也就是那串貫穿 fdp.fifa.org、cis.fifa.org 等多個內部平台的中央身分系統。註冊一個對外的 agent 帳號,等於在內部目錄裡多了一個身分。作者沒有說明這個自動加入是設計如此、還是流程漏了一道審核;可以確定的是,他從此握有一張能向那些內部平台出示的 JWT。

這個自動加入的設計,本身就值得多看一眼。把對外註冊和對內目錄接在同一個 tenant 裡,方便之處顯而易見:一套身分系統管到底,使用者不用記第二組帳號,內部團隊不用維護第二套登入。代價是信任邊界被悄悄抹平了。一個原本應該被當成「外人」的 agent 申請者,在目錄這一層,跟 FIFA 的內部員工長得一模一樣——都是 tenant 裡的一個合法身分。差別本該由角色(role)來劃,而角色這道防線,就是後來整起事件失守的地方。當「註冊成為外部使用者」和「成為內部目錄的一員」變成同一個動作,剩下唯一能擋住外人的,就只有每個內部平台自己在每個請求上做的授權檢查——而 FIFA 的後端恰好沒做。

fdp.fifa.org 是 Football Data Platform,FIFA 內部的分析與賽事管理平台;cis.fifa.org 是 Commentator Information System,餵給轉播解說的儀表板。對一個 agent 申請者來說,這兩個名字本該是看都看不到的內網。作者能連上它們、能讓前端開始為他渲染頁面,已經說明他的身分被這些平台「認得」。接下來唯一的問題只剩——認得之後,會不會擋。

這張 JWT 不是萬能鑰匙。它的 claim 裡帶著 NO_ROLES——直譯就是「此人無任何角色」。當作者試著連 fdp.fifa.org,前端的 Angular app 讀到這個 marker,做了它被教導要做的事:渲染 access-denied,把畫面擋下來。對絕大多數使用者來說,故事到此為止,因為他們看到的就是一面牆。但「看到一面牆」和「真的有一面牆」是兩回事。前者是渲染結果,後者是強制結果,兩者之間隔著一個常被忽略的問題——這道檢查到底跑在哪一端。

這裡藏著一個值得拆開的概念。一張 JWT 同時承載兩件事:authentication(你是誰)和 authorization(你能做什麼)。作者的 token 在 authentication 上是完全合法的——它由 FIFA 自家的 Entra tenant 簽發,後端驗 token 簽名時會老實地說「這個人是經過認證的」。問題出在 authorization。NO_ROLES 這個 claim 本意是「此人沒有任何角色、不該被授權做任何事」,但這層意思只有前端在讀。後端拿到一張簽名有效的 token,就把它當成綠燈放行,從沒去讀 claim 裡那句「但他什麼角色都沒有」。一個合法的身分,配上一個從不檢查授權的後端,等於把「認證通過」直接當成了「授權通過」——而這正是這類破口的共同基因。

作者用一句話定性了整起事件:「The whole thing boils down to one architectural mistake: client-side authorization with no server-side enforcement」。授權判斷只發生在 client,server 端不複查。這不是什麼新鮮的漏洞家族;它有名字,叫 broken access control,也是 IDOR、權限提升這類弱點共同的母題。它之所以一再出現,不是因為工程師不懂,而是因為前端的「藏起來」太容易被誤當成「擋下來」。下面這個模擬把這個誤解攤開:同一個 NO_ROLES 請求,在「只有前端把關」和「後端也把關」兩個世界裡,會走到完全不同的終點。

切換後端把關開關、按播放看請求流向 · 兩種強制模式

verdict:等待請求
一個 NO_ROLES 請求從 client 出發。前端守衛永遠攔不住它真正送出的 fetch——它攔的只是畫面。後端把關打開時,請求才在 API 邊界被判成 403。

一個 NO_ROLES 請求從 client 出發

授權若只在前端,NO_ROLES 請求照樣抵達後端拿到資料;後端補一道檢查,同一請求才會在 API 處被擋成 403。

同一個帳號,瀏覽器與後端說的不是同一句話

繞過前端守衛在技術上不需要什麼高深手段:後端 API 既然不檢查 role,那麼直接對它發請求,回來的就是資料,而不是那頁 access-denied。作者強調這不是 dev 環境、也不是測試資料——他原話是「This wasn't some dev environment. This wasn't test data. This was the live production Streaming Management panel」。他進到的是正式上線的 Streaming Management 面板,World Cup 2026 直播的串流控制台。

面板裡有什麼?每一場比賽的五路攝影機:PGM、Tactical、Camera1、High Behind Left、High Behind Right。還有更要命的東西——RTMP ingest 的網址。作者貼出一段示意結構(他註明這不是真的 key,只示意格式):網址結尾那串 UUID,「That UUID at the end? That's the stream key. It's shared across all five camera angles for the same match」。也就是說,攝影機的接收端網址裡,直接內嵌了能往裡推流的金鑰,而且同一場比賽的五路畫面共用同一把。下面這個對照把「瀏覽器告訴你的」和「後端實際給你的」並排——同一個 NO_ROLES 帳號,左半邊是它該看到的牆,右半邊是它其實拿得到的東西。

能看、能推流,還只是這個帳號權限清單的一部分。照作者的描述,面板裡還散落著一整排「寫入」級的操作:一個 Update Live Stats 的 modal,能編輯並發布即時統計;能修改餵進轉播系統的解說筆記;能調整官方開球時刻;甚至能竄改送給解說員的戰術陣容資料。把這些拼起來看,這不是一個「看得到敏感資料」的洩漏,而是一個「能改動正在播出的內容」的破口——統計、解說、陣容,這些都是觀眾在螢幕上直接讀到的東西。漏洞的母題還是同一個:前端把這些操作的按鈕藏給了 NO_ROLES 使用者,但後端從沒問過「你到底能不能按」。

外洩也不止串流這一條路。作者另外點到一個 Azure Function App——他把端點名遮成了 spreadsheets-api,避免直接外曝——它會回傳直接可取的 Azure Blob Storage URL,後面掛著 23 個檔案,包括轉移統計、待處理轉移的試算表這類本該鎖在內部的東西。同一種錯誤在不同服務上各犯一次:一個對外可達的 API,回傳了一條通往儲存體的直連網址,而那條網址不需要再過一道授權就能讀。讀者如果手上有自助註冊、又掛著一堆內部 API 的系統,這裡值得停一下——你的每個 Function、每個回傳 blob URL 的 endpoint,是不是都各自做了授權?

拖曳中線:左為瀏覽器畫面,右為後端回傳 · 同一帳號兩種真相

瀏覽器看到的 Access Denied JWT: NO_ROLES 後端回傳的 PGM · Tactical · Camera1 High Behind Left / Right RTMP ingest(內嵌 stream key) rtmp://in-…mediakind.com :1935/«stream key UUID» + live stats · 戰術陣容

互動圖表

同一個 NO_ROLES 帳號,瀏覽器渲染 access-denied,後端 API 卻直接回傳五路攝影機與內嵌 stream key 的 RTMP 網址。

接著是這篇文章裡最克制、也最重要的一段。作者沒有停在「拿到網址」就宣稱大功告成。他的理由很實際:「I had to confirm the preview manifests actually worked. So I copied one into VLC」——他得先確認這些 preview manifest 是真的能播的,於是挑了一個丟進 VLC。驗證的範圍劃得很清楚。確認結果是:「That's a live tactical camera feed from an active FIFA World Cup 2026 match. Playing in VLC. On my PC. In Tokyo」。東京的一台 PC,VLC 視窗裡放著一場進行中的世界盃比賽的戰術鏡頭。讀到這裡很容易往下腦補,但作者立刻把線收住:「I did not test this. I did not push anything to any RTMP endpoint」。他確認了「看得到」,但沒有去碰「推得進去」。打開 manifest 是被動讀取,往 RTMP push 是主動寫入——他停在前者,這一停就決定了整起事件是揭露而非攻擊。

「rickroll 整場世界盃」是衝擊評估,不是戰績

標題那句「rickroll 整場世界盃」確實出自原文,但它的語氣值得逐字看清楚。作者寫的是「An attacker could have rickrolled the entire FIFA World Cup. Or played Subway Surfers gameplay. Live. On every TV network worldwide」——could have,假設式。rickroll 與 Subway Surfers 是用來描述衝擊規模的修辭,不是已經發生的攻擊。推流替換畫面的機制,他也寫成條件句:「If an attacker pushed video to one of those RTMP endpoints with the stream key, they would replace the camera feed」。If/would,講的是「如果有人這麼做會怎樣」,而不是「我這麼做了」。

這個區分不是吹毛求疵,它正是負責任揭露的底線。一個資安研究者證明「我能存取到推流入口」,跟「我把畫面換成了 rickroll」是法律與倫理上完全不同的兩件事——前者是揭露,後者可能是破壞。作者把驗證停在 VLC 播放(被動讀取),沒有跨進 RTMP push(主動寫入),這條線劃在哪,整起事件的性質就定在哪。對讀者來說,這也是一個閱讀提醒:資安寫作天生帶著戲劇性,「能」與「做了」、「理論上」與「實際上」之間的助動詞,是判斷一篇揭露可不可信的關鍵。把假設語氣讀成既成事實,是最常見的誤讀。

真正落實了的衝擊,其實是另一種——資訊外洩。作者描述他能看到所有比賽的五路即時畫面、即時統計、戰術陣容,這些是已驗證的存取。至於推流替換全球轉播,那是一道「沒有人去敲、但門確實開著」的可能性。一個 NO_ROLES 帳號能走到離全球直播畫面只剩「按下推流」這一步的距離,這本身就是夠重的結論,不需要真的按下去才成立。

把已驗證和未驗證分清楚,對讀這類報告的工程師特別重要,因為衝擊評估的可信度,全靠作者願不願意誠實標註這條界線。作者在這篇裡標得很乾淨:能看見的、能用 VLC 播出來的,他寫成已發生;能推流、能 rickroll 的,他寫成假設。一篇好的安全揭露,價值不只在「找到了什麼」,更在「誠實說清楚我驗證到哪裡、沒驗證什麼」。反過來,把每一個理論上的能力都寫成既成事實,雖然讀起來更聳動,卻會讓整份報告的可信度一起貶值。這篇之所以值得當成範例,恰恰因為它在最有理由誇大的地方選擇了克制。

凌晨三點,一個人對著一整排不接電話的單位

發現漏洞只是上半場。下半場是試圖讓有能力修補的人接起電話。作者的揭露過程讀起來像一段黑色喜劇:十幾封信寄出去、有五封直接退信;WhatsApp 私訊 FIFA 的 Head of Football Technology & Data;打 FIFA 蘇黎世總機、媒體專線,都是下班的語音信箱;打達拉斯會議中心的 IBC,轉語音信箱。真正接起電話、並收下含 stream key 完整報告的,是供應商 MediaKind。之後他打給 HBS(Host Broadcast Services)被掛斷,打 CISA 的 24/7 維運線有人接、收了報告,最後在 Signal 上聯絡 FBI,也得到了回應。他自己的總結是:「They fixed it without ever responding to me. I had to call FIFA, MediaKind, HBS, CISA, and the FBI at 3am Tokyo time」。

把這串電話按順序排出來,會看到一個很具體的優先級問題。研究者手上握著一個正在進行中、影響全球轉播的漏洞,理論上最該第一時間接起電話的是漏洞的擁有者 FIFA。但 FIFA 的所有官方管道——信件、WhatsApp 給技術主管、蘇黎世總機、媒體專線——要嘛退信、要嘛沒人、要嘛轉語音信箱。第一個真正接起電話並收下含 stream key 完整報告的,反而是技術供應商 MediaKind;接著是美國的 CISA 24/7 維運線,最後是透過 Signal 聯絡上的 FBI。換句話說,這個漏洞的緊急程度,是供應商和政府機構先意識到的,正主反而最後才(被動地)反應。下面這張表把這場凌晨接力的每一通電話、每一個管道、每一個結果列出來,可以點欄位排序。

點欄位標題排序 · 7 個聯絡對象的回應狀態

聯絡對象 管道 結果
FIFA(信件 ×10+)Email5 封退信,全程未回
FIFA 技術與資料主管WhatsApp未回
FIFA 蘇黎世總機/媒體線電話下班、語音信箱
達拉斯會議中心 IBC電話語音信箱
MediaKind(串流供應商)電話接通,收下完整報告(含 stream key)
HBS / Infront電話被掛斷/無人接
CISA 24/7 維運線電話接通,收下報告
FBI 聯絡人Signal有回應

結局帶著一種荒謬的諷刺。漏洞在隔天被修了——後端開始回 403,而不是讓前端獨自擋。但 FIFA 從頭到尾沒有聯絡作者:「FIFA never responded. Not to acknowledge the report. Not to say thank you. Not to discuss compensation. Nothing」。修補與致謝是兩條沒有交集的線;有能力按下修補按鈕的組織,未必有意願承認是誰提醒了它。把這段時間線攤平來看,故事的形狀就清楚了——一個人在最該有人接電話的夜裡,對著一整排語音信箱。

而且這個「修好了」也只是補了最顯眼的那道門。作者說,事後他仍然留在 FDP 的 email 分發名單上,繼續收到官方比賽文件——Start Lists、Tactical Lineups、Full Time Match Reports,而且還是四種語言版本一份不少地寄到信箱。這是 broken access control 修補時最容易漏掉的尾巴:API 那條路被堵了,但當初因為同一個漏洞被掛上去的身分、訂閱、分發名單,沒有人回頭清。修補一個漏洞,和清掉那個漏洞造成的所有殘留狀態,是兩件工作量差很多的事;多數團隊只做了前者就鬆一口氣。事件處理完,記得回頭問一句——當初被這個漏洞授予的存取,現在全部撤乾淨了嗎?

捲入視野時依序點亮的事件時間線 · 6 個節點

註冊 agents.fifa.org 入 Entra tenant 被擋 前端 access-denied NO_ROLES 繞過 後端不檢查 直取 API 驗證 VLC 開 manifest 未推流 揭露 凌晨三點接力 FIFA 未回 隔日 修補上線 後端回 403
時間線依作者敘述重建。原文以「Night 0/隔日」描述節奏,未給確切日曆日期;節點順序忠於文章,時間間距為示意。

為什麼這類破口一再出現

把這條時間線抽乾水分,剩下的是幾條對任何後端工程師都不陌生的教訓。第一條最硬:授權必須在後端強制,且必須對每一個 endpoint 獨立強制。前端的角色判斷只是 UX——它決定使用者看到什麼按鈕,但它跑在攻擊者完全掌控的環境裡,任何「藏起來」都能被繞過。前端隱藏不等於後端拒絕,這句話聽起來像常識,卻是 broken access control 年復一年蟬聯 OWASP 前段班的原因。

第二條關於信任邊界。NO_ROLES 這個帳號最危險的地方,不是它有太多權限,而是系統把「對外註冊的身分」和「對內平台的身分」混進了同一個 tenant。一個 agent 報名表單,不該成為進入 Football Data Platform 的入場券。當自助註冊的身分能向內部 API 出示一張被接受的 JWT,真正該問的是:這張 token 在每個 endpoint 上,到底被當成「已認證」還是「已授權」?認證回答「你是誰」,授權回答「你能做什麼」——把前者的通過誤當成後者的通過,正是 IDOR 與權限提升的共同裂縫。

作者把後端的行為講得很白:「The backend APIs trust any authenticated tenant member and serve data regardless of roles」——後端信任任何一個通過認證的 tenant 成員,不管 role 就把資料端出去。後端把「token 簽名有效」讀成了「這個人有權拿資料」:認證這關該亮綠燈、授權這關該亮紅燈,只看前者,兩盞燈就短路成一盞。下面這個小工具把 token 拆成兩問,看同一個 NO_ROLES claim 在兩問上本該答得完全相反,而破口就是後端漏掉了第二問。

對同一張 token 問兩個不同問題 · 認證與授權各自的答案

{ "sub":「agent-公開註冊」, "roles":"NO_ROLES" }
點上面任一個問題,看後端應該怎麼答。
同一張 token、兩個問題:認證該放行、授權該拒絕。FIFA 的後端只問了第一個,於是把「認證通過」當成了「授權通過」。

同一張 token、兩個問題:認證該放行、授權該拒絕

點兩個問題:同一張 NO_ROLES JWT 在「你是誰」該過、在「你能做什麼」該擋;後端只問了前者就是破口。

第三條是關於憑證擺在哪裡。stream key 內嵌在 RTMP ingest 網址裡、五路攝影機共用一把,意味著「能讀到網址」和「能寫入推流」之間沒有第二道牆。讀取權限一旦外洩,寫入能力跟著一起走。把寫入金鑰和讀取資料放在同一個回應裡,等於假設「拿得到網址的人都是好人」——這正是 client-side authorization 一路犯下的同一個假設,只是換了地方再犯一次。作者沒把這假設按到底,但門開著就是門開著。

一條 RTMP 網址裡同時藏著讀與寫 · 為何讀取外洩就帶走寫入

一條回應、同一個字串 rtmp://in-…mediakind.com:1935/ «stream key UUID» 能讀到(位址) 就等於能寫入(推流) 讀取權限一外洩,寫入能力跟著一起走——中間沒有第二道牆。
結構依作者所貼示例重繪;作者註明那串 UUID 是示意格式、非真實 key。五路攝影機共用同一把。

最後一條不在程式碼裡,而在流程裡。整篇文章最刺眼的不是漏洞本身——broken access control 每天都在發生——而是發現漏洞的人找不到人通報。十幾封信、五封退回,總機下班、媒體線下班,最後接起電話的是供應商 MediaKind 和政府的 CISA、FBI,正主 FIFA 一路沉默。一個賽事級的組織,在世界盃進行中的當下,沒有一條讓外部研究者能在凌晨三點按下去就有人接的揭露通道,這本身就是一個系統設計缺陷,只是它的「系統」是組織而不是程式。安全不只是後端那道 403;它也包括:當有人善意地發現你漏了那道 403,他知道該敲哪扇門、而那扇門後面有人。你的 security.txt、你的回報信箱、你的值班輪替,是不是真的有人在另一端?

把四條教訓收攏起來,其實是同一句話在不同層次上重複:不要把「看起來被擋住了」當成「真的被擋住了」。前端的 access-denied 頁面看起來像一道牆,其實只是一張畫;認證通過看起來像授權通過,其實只回答了一半的問題;漏洞修好了看起來像事件結束,其實殘留的身分和訂閱還掛在那裡;組織有一堆官方聯絡管道看起來像隨時能通報,其實凌晨三點全是語音信箱。每一層的錯誤都長一個樣子——把表象的「不可見、不可達、已處理」誤當成實質的「被強制阻擋」。這名研究者做的,不過是逐層去敲,然後發現每一道看似鎖著的門,背後都沒有真的上鎖。

The lesson:前端可以決定使用者看到什麼,但不能決定使用者拿得到什麼——每一個 endpoint 的授權,都得在後端、在伺服器這一側,獨立地再問一次「你能做什麼」。