1 điểm bởi GN⁺ 6 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • -- của Git không phải là dấu kết thúc tùy chọn thông thường mà dùng để phân tách revision và pathspec, vì vậy để truyền một revision không đáng tin cậy một cách an toàn thì cần --end-of-options, được hỗ trợ từ Git 2.24.0
  • Trong git log --end-of-options "$rev" -- "$path", dấu ở phía trước phân tách tùy chọn và revision, còn -- ở phía sau phân tách revision và đường dẫn, và hai dấu này không thể thay thế cho nhau
  • Ngay cả khi chạy trực tiếp mảng argv mà không qua shell, nếu đầu vào bắt đầu bằng dấu gạch ngang bị diễn giải thành các tùy chọn như --upload-pack, core.sshCommand, ProxyCommand thì vẫn có thể xảy ra CWE-88 argument injection
  • Trong 19 trình quản lý gói được khảo sát, 17 trình chạy nhị phân Git theo cách mặc định hoặc là cách duy nhất, nhưng công cụ dùng --end-of-options thì chỉ có cmd/go của Go
  • Biện pháp xử lý tận gốc có chi phí tương thích là phải nâng phiên bản Git tối thiểu lên 2.24.0, 2.30.0 hoặc 2.43.1 tùy subcommand; còn thư viện Git thì loại bỏ ranh giới argument injection nhưng đổi lại phải tự bám theo các bản vá an toàn checkout từ upstream

Khác biệt giữa ----end-of-options

  • Trong các công cụ Unix thông thường, -- đánh dấu kết thúc việc phân tích tùy chọn, nên rm -- -f sẽ coi -f là tên tệp thay vì tùy chọn xóa bắt buộc
  • Git từ rất sớm đã dùng -- làm dấu phân cách giữa revision và pathspec
    • git log foo là mơ hồ, vì không rõ foo là nhánh hay tệp
    • git log main -- README.md nghĩa là các commit trong main có thay đổi README.md
  • Vì thiết kế này nên ở vị trí revision không có dấu kết thúc tùy chọn, và trong git log "$rev", nếu $rev bắt đầu bằng dấu gạch ngang thì Git sẽ diễn giải nó như một tùy chọn
  • Commit đưa vào --end-of-options giải thích rằng vì -- hiện có đã dùng để ngăn cách revision và pathspec, nên cần một dấu riêng để phân tách tùy chọn và revision
  • --end-of-options được ghi trong tài liệu gitcli(7) và được thêm vào tháng 11/2019, khi Git 2.24.0 phát hành

Cách dùng đúng theo từng lệnh

  • git clone -- "$url" hoạt động vì clone tuân theo quy ước POSIX, nên -- trước URL sẽ kết thúc việc phân tích tùy chọn
  • git checkout "$ref" -- thì -- phía sau dùng để chỉ ra $ref là revision chứ không phải tên tệp, nhưng không ngăn được việc $ref bị diễn giải là tùy chọn ngay từ đầu
  • Để truyền an toàn đồng thời revision và đường dẫn không đáng tin cậy, cần dùng cả hai dấu như trong git log --end-of-options "$rev" -- "$path"
    • --end-of-options phân tách tùy chọn và revision
    • -- phân tách revision và đường dẫn
  • Nếu coi hai dấu này có thể thay thế cho nhau thì sẽ không chặn được đầu vào bắt đầu bằng dấu gạch ngang

Mốc hỗ trợ khác nhau theo từng subcommand

  • Hỗ trợ --end-of-options không được áp dụng cho mọi lệnh Git cùng lúc mà được bổ sung riêng theo từng subcommand
  • git rev-parse dùng bộ phân tích argument riêng, nên bắt đầu hỗ trợ ở Git 2.30.0, muộn hơn 1 năm so với lần giới thiệu ban đầu
  • git checkoutgit reset tự diễn giải --, và vì cách triển khai ban đầu giữ lại --end-of-options trong danh sách argument nên đã từ chối nó
    • Vấn đề này được sửa vào tháng 2/2024, khi Git 2.43.1 phát hành

Argument injection vẫn xảy ra dù không dùng shell

  • Git, Mercurial và SSH đều có sẵn các tùy chọn cho phép chạy lệnh do bên gọi chỉ định
    • git clone --upload-pack=<cmd> chỉ định nhị phân phía máy chủ
    • -c core.sshCommand=<cmd> trong mọi lệnh Git sẽ thay đổi lệnh kết nối
    • --config=alias.<subcmd>=!<shell> của Mercurial ghi đè subcommand sẽ chạy bằng một shell script tùy ý
    • -oProxyCommand=<cmd> của SSH chỉ định lệnh proxy
  • Nếu chương trình bao bọc đưa chuỗi không đáng tin cậy vào danh sách argument, các tính năng này có thể biến thành công cụ tấn công
  • Kiểu thất bại này thuộc CWE-88argument injection, khác với shell command injection
    • Nó vẫn xảy ra dù chương trình dùng mảng argvexec thay vì system()
    • Mảng được truyền nguyên vẹn sang Git, nhưng Git lại diễn giải argument bắt đầu bằng dấu gạch ngang như tùy chọn
  • CVE-2019-13139 của docker build là một trường hợp dùng os/exec của Go và mảng argv mà không đi qua shell
    • Phần #ref:dir của Git context URL được truyền vào git fetch origin <ref>, khiến <ref> bị diễn giải thành --upload-pack=<cmd>

Lỗ hổng lặp lại trên nhiều hệ thống quản lý phiên bản

  • Cùng một ngày trong tháng 8/2017, mô hình giống nhau này được công bố trên bốn hệ thống quản lý phiên bản
  • Cả bốn hệ thống đều truyền hostname trong URL làm argument cho SSH, và hostname bắt đầu bằng -oProxyCommand= sẽ bị xử lý như một tùy chọn SSH
  • Theo phân tích hậu sự cố của Phabricator, trong ba công cụ còn được duy trì tích cực khi đó, chỉ Subversion thêm -- trước hostname
    • Git và Mercurial thì xác thực định dạng hostname, một phần vì không phải mọi triển khai SSH đều hỗ trợ --
  • Ngay cả mã thiếu -- cũng trông và hoạt động bình thường cho đến khi argument bắt đầu bằng dấu gạch ngang, nên cơ chế này về bản chất là không an toàn mặc định

Con đường phơi lộ qua trình quản lý gói

  • Trình quản lý gói nhận Git URL hoặc ref từ manifest, lockfile và metadata của dependency bắc cầu rồi truyền chúng cho tiến trình con
    • gem 'foo', git: '...' trong Gemfile
    • github:user/repo#ref trong package.json
    • Các cấu hình tương đương trong pyproject.toml, Cargo.toml, mix.exs, Package.swift, pubspec.yaml, conanfile.py, go.mod
  • Tính đến HEAD tháng 7/2026, trong 19 trình quản lý gói được khảo sát thì có 17 trình chạy nhị phân Git theo đường mặc định hoặc là đường duy nhất
  • Hai trình còn lại mặc định dùng thư viện
    • Cargo dùng libgit2 và sẽ chạy tiến trình Git nếu bật net.git-fetch-with-cli
    • Poetry chuyển sang dulwich từ 1.2.0 và có thể dùng Git hệ thống qua cấu hình system-git-client
  • Nix dùng libgit2 khi đọc kho cục bộ, nhưng khi fetch thì chạy tiến trình Git vì libgit2 không hỗ trợ helper git-credential
  • Danh sách khảo sát gồm Bundler, Cargo, CocoaPods, Composer, Conan, Go, Helm, Homebrew, Mix, Nix, npm, pip, pnpm, Poetry, Pub, SwiftPM, uv, vcpkg, Yarn

Các CVE đã ghi nhận ở trình quản lý gói

Tình hình phòng vệ thực tế và bản vá của Go

  • Trong 17 trình quản lý gói chạy tiến trình Git, công cụ dùng --end-of-options chỉ có cmd/go của Go
  • Tháng 6/2019, Go thêm -- trước URL kho chứa như một biện pháp tăng cường phòng thủ chung
  • Đến tháng 1/2026, người ta nhận ra chỉ -- là chưa đủ, nên bản vá cho CVE-2025-68119 đã bổ sung --end-of-options một cách rộng khắp
  • Cùng bản vá đó còn có HGPLAIN=+strictflags
    • Thiết lập này đã hạn chế việc diễn giải tùy chọn giai đoạn đầu của Mercurial từ khi Mercurial 4.4.2 phát hành năm 2017
  • Commit vá của Go lưu ý rằng có thể cần thay đổi mang tính cấu trúc hơn để tránh tái đưa lại cùng vấn đề, nhưng trước mắt nó giải quyết được lỗi hiện tại

Hầu hết cơ chế phòng vệ chỉ được thêm sau khi có lỗ hổng công khai

  • Các trình quản lý gói còn lại, dù có bảo vệ danh sách argument, chủ yếu vẫn dùng -- hoặc từ chối dấu gạch ngang ở đầu đầu vào
  • -- trước URL trong git clone của Bundler được thêm vào bởi bản vá CVE-2021-43809
  • Việc cocoapods-downloader từ chối dấu gạch ngang đầu vào được triển khai qua ba commit trong 10 ngày tháng 3/2022, trùng thời điểm công bố CVE-2022-21223
  • Cơ chế phòng vệ của Poetry được thêm vào tháng 9/2021, được gán CVE sau đó 1 năm, và 6 tháng sau thì chuyển sang dulwich
  • vcpkg là ngoại lệ khi dùng -- ngay từ ngày đầu viết hỗ trợ Git registry

Ràng buộc tương thích do phiên bản Git tối thiểu

  • Khuyến cáo về CVE-2022-24828 của Composer chỉ ra --end-of-options là cách sửa đúng, nhưng vì phải hỗ trợ cả Git cũ hơn nên họ chọn cách từ chối tên nhánh bắt đầu bằng dấu gạch ngang
  • Tích hợp Git của vcpkg ghi rõ phiên bản tối thiểu là Git 2.7.4, còn HOMEBREW_MINIMUM_GIT_VERSION trên Linux của Homebrew là 2.7.0 được đặt từ năm 2018
  • Amazon Linux 2, bản phân phối từng cung cấp Git 2.14.3, hết vòng đời vào tháng 6/2026, nên các bản phân phối mà các ngưỡng tối thiểu này từng phải bám theo hiện mới bắt đầu rời khỏi phạm vi hỗ trợ
  • Trạng thái hỗ trợ dài hạn của Ubuntu cũng khiến việc chuyển đổi đồng loạt trở nên khó khăn
    • Ubuntu 18.04 cung cấp Git 2.17.0 và được hỗ trợ mở rộng đến 2028
    • Ubuntu 20.04 cung cấp Git 2.25.1 và được hỗ trợ mở rộng đến 2030
    • Git 2.25.1 chấp nhận --end-of-options với git fetch, nhưng từ chối nó với git rev-parse
  • Muốn dựa vào --end-of-options thì phần lớn subcommand cần tối thiểu Git 2.24.0, rev-parse cần 2.30.0, còn checkoutreset cần 2.43.1
  • Nâng phiên bản tối thiểu đồng nghĩa không còn hỗ trợ người dùng đang dùng Git cũ đi kèm trong các bản phân phối

Dùng thư viện Git thay vì chạy tiến trình

  • libgit2, gitoxide, go-git, JGit, dulwich đều triển khai giao thức truyền tải Git cần cho clone và fetch ngay trong tiến trình
  • Không có ranh giới argv riêng, nên không tồn tại đối tượng để chèn vào danh sách argument
  • Jujutsu dùng gitoxide để tích hợp Git và chưa có CVE công khai nào thuộc kiểu argument injection
    • Hai khuyến cáo hiện có chỉ liên quan đến path traversal và việc thư viện kế thừa thiếu kiểm tra va chạm SHA-1
  • CVE-2025-21613 của go-git chỉ giới hạn ở truyền tải file://
    • Đây là đường mã duy nhất trong go-git có chạy nhị phân Git
  • Khi đóng gói một triển khai Git riêng, bạn phải tự theo dõi mọi bản vá an toàn checkout mà upstream Git phát hành; cả libgit2 lẫn JGit đều từng lặp lại các bản sửa liên quan lĩnh vực này
  • Chi phí đó là có thật, nhưng đổi lại vấn đề chuyển từ việc phải nhớ kiểm tra argument vĩnh viễn ở từng điểm gọi sang việc áp dụng luồng bản vá upstream cụ thể

Phạm vi thay đổi được đề xuất cho Homebrew

  • PR của Homebrew nâng phiên bản Git tối thiểu lên 2.30.0 và thêm --end-of-options tại các vị trí sau
    • trước URL của clone, remote set-url, ls-remote
    • trước ref của rev-parse
  • Các lời gọi checkoutreset không được thay đổi
    • Muốn bảo vệ cả hai lệnh này thì cần Git 2.43.1, phát hành tháng 2/2024
    • Phiên bản này mới hơn Git mà nhiều bản phân phối hiện còn hỗ trợ đang cung cấp

1 bình luận

 
Ý kiến trên Lobste.rs
  • Dạo này mình ngày càng mê jj. So với sự rối rắm của Git thì đúng là thấy nhẹ đầu hơn; mình vẫn đang học, nhưng nó phản ánh ý định rất tốt và cũng dễ nắm được cách làm việc mong muốn
    Đặc biệt, kiến thức học được từ một lệnh jj có thể tự nhiên áp dụng sang các lệnh khác. Tài liệu git log thì số lượng cờ theo từng tùy chọn đầu ra bùng nổ, lại còn trộn lẫn “ký pháp đặc biệt” cho cú pháp phạm vi commit với các cờ lọc bổ sung, và cũng có rất ít kiến thức chuyển sang được các lệnh Git khác
    Trong khi đó, tài liệu jj log ngắn gọn đến mức một trang còn dư chỗ. Nó thay thế kiểu phức tạp chắp vá của Git bằng ba thứ là tập revision, tập file và DSL mẫu đầu ra, và chúng được dùng nhất quán trên toàn bộ jj nên đơn giản hơn nhiều, dễ kết hợp hơn và trực quan hơn. Tất nhiên cũng phải tính đến chuyện Git đã tích lũy đủ thứ rườm rà suốt 20 năm, nhưng jj có vẻ ở vị thế thuận lợi hơn nhiều để tránh điều đó
  • Mình biết trên dòng lệnh thường cần -- trước các đối số do người dùng cung cấp, nhưng nếu còn đòi hỏi thêm cả quy tắc biến thể riêng ở đây thì đúng là một cái bẫy nguy hiểm quá mức
  • Đây là kết quả tất yếu của việc làm công cụ trở nên quá phức tạp. Git trông như Bash của phiên bản mới vậy
  • Mình tò mò vì sao ban đầu họ lại chọn -- để giải quyết sự mơ hồ của đối số file. Không rõ có đánh đổi thiết kế nào khó nhìn thấy từ bên ngoài khiến họ chọn cách này thay vì kiểu đơn giản là nhận đối số file một cách tường minh hay không
    • Dùng quy ước phân tích tùy chọn của UNIX vốn đã được thiết lập cho một mục đích hoàn toàn khác thì đúng như dự đoán là một lựa chọn ngớ ngẩn, rất đúng chất Git
  • Triết lý mọi thứ đều là văn bản của UNIX lại tiếp tục gây rắc rối. Dòng lệnh là ví dụ hoàn hảo của dữ liệu có cấu trúc, nhưng có vẻ chi phí phối hợp chung để thoát khỏi cái hố này là quá lớn
    Nhìn lại tranh luận giữa Tcl và Scheme trong thập niên 1990, có lẽ trong trường hợp này “càng tệ càng tốt” lại đúng. Trong khi các hệ S-biểu thức, bao gồm cả JSON và XML, hầu như không bám rễ được trong hệ sinh thái UNIX, thì Tcl lại có một cách tiếp cận tương đối có nguyên tắc: xếp chồng các ngôn ngữ con bên trong chuỗi ký tự