Baseten lộ token admin GitHub 3 năm trong Docker build history

Một agent bảo mật tự động mất 25 phút để lấy được quyền admin trên các repo GitHub nội bộ của Baseten — công ty inference vừa được định giá 13 tỷ USD. Không cần credential, không cần source code, chỉ cần domain công khai.
Chuyện gì đã xảy ra
Strix, công ty làm agent pentest tự động, đang cân nhắc dùng Baseten làm nhà cung cấp inference cho sản phẩm của họ. Trước khi gửi dữ liệu và mô hình cho bên thứ ba, họ có thói quen tự quét hạ tầng đối tác trước — và lần này họ chạy công cụ của chính mình nhắm vào *.baseten.co, không có credential hay quyền truy cập source.
Agent tìm ra một registry Harbor tại một subdomain ít ai để ý, với một project để public. Từ đó nó pull được image baseten/baseten-app mà không cần đăng nhập. Bên trong image có một cặp AWS key — nhưng key đó đã chết. Agent tiếp tục chạy TruffleHog quét các layer, rồi soi thẳng vào config của image thay vì chỉ file hệ thống, và tìm thấy một GitHub personal access token nằm trong trường history[].created_by — phần ghi lại lệnh build, không phải file thật trong container.
Token đó thuộc về tài khoản basetenbot, có scope repo đầy đủ, và thuộc tổ chức basetenlabs. Nó có quyền admin/push trên repo sản phẩm chính, trên repo GitOps điều khiển cluster production, trên Homebrew tap phân phối CLI, và quyền đọc/ghi trên nhiều repo private khác — bao gồm cả các thư mục đặt tên theo từng khách hàng cụ thể. Image chứa token này được build từ tháng 3/2023. Khi Strix tìm thấy vào tháng 7/2026, token vẫn còn hoạt động — hơn ba năm sau.
Vì sao lỗi này khó thấy bằng mắt thường
Nguyên nhân gốc là một pattern Dockerfile quen thuộc: build cần fetch dependency private từ GitHub, nên ai đó truyền token qua build argument rồi dùng nó để cấu hình git config --global cho phiên build đó.
# conceptual example — not a real tool
ARG GITHUB_TOKEN
RUN GITHUB_TOKEN=${GITHUB_TOKEN} bash -c '\
if [[ "${GITHUB_TOKEN}" != "" ]]; then \
git config --global --add \
url."https://${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:"; \
fi'
Vấn đề không nằm ở filesystem cuối cùng của image — dọn sạch file cấu hình sau khi build không giải quyết được gì. Docker ghi lại toàn bộ lệnh RUN vào history của image config, và giá trị build argument bị expand thẳng vào đó. Config này được tải về cùng lúc với image, tách biệt hoàn toàn khỏi các layer filesystem thông thường — nên một lượt scan chỉ soi file trong container sẽ bỏ sót nó hoàn toàn. Docker đã cảnh báo về hành vi này từ lâu, nhưng pattern trên vẫn phổ biến vì nó đơn giản và chạy được ngay.
Điều cần biết
Baseten xử lý sự cố nhanh: xác nhận mức độ nghiêm trọng "critical" trong vòng chưa đầy một ngày, khóa registry và thu hồi token ngay buổi chiều hôm sau. Nhưng bài học không nằm ở tốc độ phản ứng, mà ở việc lỗ hổng tồn tại được ba năm rưỡi mà không ai phát hiện.
- Build history là bề mặt rò rỉ riêng, tách biệt khỏi filesystem image. Chạy
docker history --no-trunchoặc đọc trực tiếp trườnghistory[].created_bytrong config blob — không chỉ scan layer file như thông thường. - Fix đúng là dùng BuildKit secret mount, không phải build argument. Secret mount không ghi giá trị vào layer hay history, và không tồn tại sau khi bước build kết thúc.
- Token cần scope tối thiểu và có hạn dùng. Một token chỉ cần quyền đọc dependency lại được cấp quyền admin/push trên repo sản phẩm và GitOps — đây là lỗi phân quyền, không chỉ lỗi rò rỉ.
- Xóa Dockerfile không xóa được image đã public. Ai đã pull image cũ vẫn giữ được token trong đó — nên bước bắt buộc sau khi phát hiện là thu hồi token, không phải chỉ sửa Dockerfile cho lần build tiếp theo.
- Registry riêng cần audit quyền public theo project, định kỳ. Project Harbor bị lộ không phải do cấu hình sai một lần — nó là một góc hạ tầng bị quên, đúng kiểu subdomain "không ai nhớ tới" mà mọi recon tool đều nhắm vào đầu tiên.
Baseten dùng AI security tooling và có đội bảo mật phản ứng nhanh, nhưng token từ 2023 vẫn nằm đó chờ được tìm ra. Câu hỏi đáng đặt ra cho mọi team đang build container image: image cũ nhất trong registry của bạn còn giữ gì trong build history?
Bài viết được hỗ trợ bởi AI (Amy 🌸). Nội dung đã được kiểm duyệt bởi tác giả.
Related Posts
Podman: Container Không Cần Daemon, Bảo Mật Hơn Docker?
Podman chạy container không cần daemon root, giảm bề mặt tấn công và tương thích hoàn toàn với Docker CLI. Đã đến lúc chuyển đổi?
3,800 GitHub Repos Bị Đánh Cắp Qua VSCode Extension: Bài Học Cho Developer
GitHub xác nhận 3,800 repository nội bộ bị lộ sau khi nhân viên cài extension VSCode chứa mã độc. Đây là hồi chuông cảnh tỉnh cho mọi developer đang dùng VSCode.
AI Bot Spam Đang Giết Chết Open Source: Câu Chuyện Từ Archestra
Một startup Y Combinator nhận 253 comment rác từ AI bot chỉ trong 1 issue, 27 PR không qua test cho 1 feature duy nhất. Đây là vấn đề thực sự mà mọi maintainer đang phải đối mặt.