
※本郵件是寄給 Times Car 會員中,已確認其身分驗證文件資訊外洩的會員。
正如這封郵件史上堪稱頂級恐怖的開頭文所示,沒錯,個人資料出事了。

而且連駕照影像也外洩了(不妙了!)。
因此這次,我想針對這起意外成為受害者的事件,從外部可見的實作切入,盡可能以一介平凡個人的角度,做一些微小而謹慎的原因推測。
畢竟這裡是 Qiita,所以我也會順便加入一些安全程式設計的元素一起討論。
2026 年 9 月 28 日,Park24 發表指出,因為遭到 Times Car Web 系統不正當存取,約 660 萬筆帳號資訊被第三方取得。對象除了 Times Car 與 Times Business Service 的會員之外,也包含已退會的使用者,以及尚未完成加入 Times Car 的申請者。
外洩資訊包含姓名、地址、出生日期、電話號碼、電子郵件地址、駕照資訊、身分驗證文件資訊、密碼、串接服務 ID 等,且項目會因對象不同而有所差異。

就我自己的情況,開頭那封郵件提到的是「駕照影像」、「現住址確認文件影像」,看起來是依照外洩資訊不同,郵件內容也會有所差異吧。
下面是簡單的時間軸。
公布日確認內容9月25日 第1報同日 9:07 偵測到不正當存取。公布可能發生個資外洩9月28日 第2報確認約 660 萬筆帳號資訊遭取得。說明已於 9 月 26 日 7:25 前切斷路徑與攻擊來源之間的通信9月29日 第3報確認外洩身分驗證文件的帳號約 160 萬筆。同日起開始對象會員的個別郵件通知9月29日的第3報中,已確認身分驗證文件外洩的帳號數約為 160 萬筆。已確認外洩的資訊包含駕照影像、現住址確認文件影像,另外還有學生方案的學生證影像、家族方案的家族確認文件影像。同日起開始向對象會員寄送郵件通知,個別外洩內容的詳細資訊預計於約 2 週後再另行通知。
接下來,我會以公開 HTML 與官方技術資料為出發點進行推測。假設中的架構與缺失並不確定,並不能保證就是 Times 實作上的瑕疵,還請見諒。
9 月 29 日取得的 Times Car 首頁 HTML 中,能看到對下列 JavaScript 的參照。這裡摘錄 src 屬性如下。
<script src="/view/teedaExtension/org/seasar/teeda/ajax/js/ajax.js?20201217"></script>
也成功取得參照的檔案,內容中包含 Kumu.Ajax 與 executeTeedaAjax 這兩個識別字。至少可以確認,首頁確實有參照 Teeda 相關的 JavaScript,而且網站也有在提供這個檔案。
Teeda 是 Seasar 專案中的 Java Web 應用程式框架。官方文件中似乎把它分成處理畫面的 Teeda Core、處理 HTML 樣板的 Teeda Extension、以及處理非同步通訊的 Teeda Ajax。不過,Teeda Ajax 可以獨立於其他模組使用,所以光靠這段 JavaScript,還不能像在逆轉裁判裡一樣直接把整個伺服器採用的技術一口氣指認出來(看招!)。
Seasar 專案已公告,除例外列出的產品外,會在 2016 年 9 月 26 日結束維護與支援。當然,Teeda 和 Seasar2 也都包含在 Seasar 專案裡。
即使官方支援結束,程式當然還是可能繼續跑,但新的官方修補程式就不會再有了。大家或許也有經驗,一旦要持續使用,確認弱點資訊、自行修補,再加上更新相依函式庫等等,維運工作就變得更加重要。老實說,多半都會考慮移轉。
身為一名工程師,我比較在意的不是框架的新舊,而是處理個資的流程,當時是否有持續檢查與修正的體制。畢竟現實中,仍然有很多團隊在維運遺留系統。
身為一名使用者,我想拜託的是,請把舊框架移到新的版本吧。
從這裡也可以提出另一種可能:沒有套用適當修正,因而被已知弱點擊中。
以會員網站的常見架構來看,可以先假設:瀏覽器送出請求,由 Web 應用程式接收,再去查詢會員資訊與身分驗證影像的儲存位置。影像是放在資料庫裡,還是放在別的儲存位置,官方都尚未公布,所以這裡先只看角色分工。

以下內容混雜了一些安全程式設計的超基本觀念,但因為也不能排除是原因之一,所以還是先列出來。
Teeda Ajax 透過 JavaScript 端的 callback 函式命名規則,來解析伺服器端的元件與方法。若是從瀏覽器呼叫伺服器處理的架構,入口處也必須實作認證與授權。
認證是確認使用者是誰的處理;授權則是判斷是否允許該使用者對目標資料進行操作。即使已經登入(也就是已通過認證),如果沒有授權,還是不能取得別人的駕照影像。
例如,畫面初始顯示時有確認登入,但若負責圖片取得或搜尋的非同步處理沒有做認證與授權檢查,資料就可能從那裡被取走。光把畫面上的按鈕藏起來,無法阻止伺服器直接收到的請求。授權不能依賴畫面跳轉,而是要套用在每一個回傳資料的處理上。
這個對策不只適用於 Teeda,OWASP 也指出,應該在包含非同步通訊的所有請求中確認權限,原則上拒絕不符合許可條件的存取。這是工程師最基本的常識。
另一種可能是,把搜尋條件或會員編號以字串方式串接進 SQL。當外部輸入進入 SQL 語法後,就有可能造成 SQL Injection,進而改變原本預期的搜尋條件。
這裡因為若直接以真實服務為例會有很多問題,所以改用 Python 與 SQLite 示範取得條件的寫法。login_member_id 是伺服器從已驗證的 session 中確定的本人 ID,不能把瀏覽器指定的會員 ID 直接傳進去。
def get_document_key(conn, *, login_member_id, document_id):
# 在伺服器端驗證文件 ID 的型別與範圍
if type(document_id) is not int or document_id <= 0:
raise ValueError("invalid document id")
# 將本人 ID 納入搜尋條件,值不要串接進 SQL 語法
row = conn.execute(
"SELECT storage_key FROM identity_documents "
"WHERE id = ? AND member_id = ?",
(document_id, login_member_id),
).fetchone()
if row is None:
raise PermissionError("document not accessible")
return row[0]
對 ? 綁定值,是為了避免輸入被當成 SQL 指令;而 member_id = ? 則是限制只能取得本人擁有的文件。兩者都需要,單純防住 SQL Injection,並不足以阻止別人資料被拿走。
想更廣泛了解這類內容的話,可以參考這篇文章。
也可能是透過相依函式庫或執行環境的弱點、管理用認證資訊遭竊、或設定不當而入侵。若攻擊者能在伺服器上執行任意程式,就可能不經過畫面層的授權,直接以應用程式本身的權限存取保存的資料。
不過,這次是否真的成立了任意程式碼執行,官方並沒有公布,所以目前不明。後續也不確定會不會再釋出更多資訊。
以約 660 萬筆的規模來看,除了要防範入侵路徑,也很想進一步思考:即使被入侵,還能取得多少資料。當然,像是所有資料是否存放在同一個資料庫、是否透過一次操作就被取出,這些內部結構與取得流程都無法斷定就是了。
一般可考慮的做法是,僅賦予會員頁面所需欄位的存取權,不允許匯出全部會員資料或一次取得所有身分驗證文件。即使是資料庫的唯讀權限,只要能讀出全體會員個資,從外洩防護的角度來看仍然太強。
也可以把身分驗證影像獨立成與一般會員資訊分開的儲存與取用機制,只允許業務上必要的使用者與處理存取。這不只是換儲存位置而已,實際使用的驗證資訊、可取得的範圍、回傳欄位都要一起限制。

為了能夠察覺大量取得,也應把取得件數、流量、以及與平常不同的操作納入監控。就算單次回應有上限,只要能反覆取得,資訊還是會被累積,所以也要確認每位使用者或每個處理的取得量與頻率。最好還要準備好偵測後立即停止權限的程序。
這次受影響範圍似乎還包含已退會的使用者與尚未完成加入的申請者。這是理所當然的,因為持續保存的資料,也會在外洩時成為受害對象。因此,應該先決定資料保存的必要性與期間,並把刪除不再需要資料的流程也納入規格。
關於密碼,第2報說是以無法還原的形式保存,但具體方式與設定並未公布。一般來說,如果雜湊值外洩,攻擊者就能嘗試將候選密碼計算後的結果與之比對,因此需要使用專門給密碼用途、計算成本較高的方式,並為每位使用者加入 salt(避免 rainbow table 的措施)。到底用了多強的雜湊方式,會大大影響整件事的判斷。
雖然保存資料的加密也很重要,但如果應用程式能取得解密後的資料,那透過應用程式發生的外洩仍然可能存在。即使已加密,限制只讓必要資料被取用的權限設計仍然是個課題。
感謝看到這裡。
雖然以前也不是沒有過地址或姓名外洩的事件,但連駕照影像都一起外洩,真的不常聽到(或者說,根本不想聽到)對吧。
而且話說回來,我其實剛好前天才剛辦完駕照更新,所以新拍的照片那些更新後的駕照我還沒登錄到 Times,那麼對我來說,算是不幸中的大幸吧。
不過更新時也有一些不該變動卻已經變動的資訊流出,所以接下來還是得多加小心地過日子……。
那麼,下次再見。