1 คะแนน โดย GN⁺ 6 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • -- ของ Git ไม่ใช่ตัวจบตัวเลือกแบบทั่วไป แต่ใช้เพื่อ แยกระหว่าง revision กับ pathspec ดังนั้นหากต้องการส่ง revision ที่ไม่น่าเชื่อถืออย่างปลอดภัย จำเป็นต้องใช้ --end-of-options ซึ่งรองรับตั้งแต่ Git 2.24.0
  • ใน git log --end-of-options "$rev" -- "$path" เครื่องหมายตัวแรกใช้แยก ตัวเลือกกับ revision ส่วน -- ตัวหลังใช้แยก revision กับ path ทั้งสองใช้แทนกันไม่ได้
  • แม้จะรัน argv array โดยตรงโดยไม่ผ่าน shell หากอินพุตที่ขึ้นต้นด้วยขีดถูกตีความเป็นตัวเลือกอย่าง --upload-pack, core.sshCommand, ProxyCommand ก็อาจเกิด CWE-88 argument injection ได้
  • จาก package manager 19 ตัวที่สำรวจ มี 17 ตัวที่เรียกใช้ไบนารี Git เป็นวิธีหลักหรือวิธีเดียว แต่มีเครื่องมือที่ใช้ --end-of-options เพียง cmd/go ของ Go ตัวเดียว
  • การแก้ที่รากเหตุมีต้นทุนด้านความเข้ากันได้ เพราะต้องยกระดับเวอร์ชันขั้นต่ำของ Git เป็น 2.24.0·2.30.0·2.43.1 ตาม subcommand ส่วน Git library แม้จะตัดขอบเขตที่ทำให้เกิด argument injection ออกไป แต่ต้องติดตามแพตช์ความปลอดภัยของ upstream โดยตรงในเรื่อง checkout

ความต่างระหว่าง -- กับ --end-of-options

  • ในเครื่องมือ Unix ทั่วไป -- ใช้ระบุจุดสิ้นสุดของการตีความตัวเลือก ดังนั้น rm -- -f จะปฏิบัติกับ -f เป็นชื่อไฟล์ ไม่ใช่ตัวเลือกบังคับลบ
  • Git ใช้ -- มาตั้งแต่ช่วงแรก ๆ ในฐานะ ตัวคั่นระหว่าง revision กับ pathspec
    • git log foo กำกวมว่า foo หมายถึง branch หรือไฟล์
    • git log main -- README.md หมายถึง commit ใน main ที่แตะ README.md
  • ด้วยการออกแบบนี้ ตำแหน่ง revision จึงไม่มีเครื่องหมายจบตัวเลือก และใน git log "$rev" หาก $rev ขึ้นต้นด้วยขีด Git จะตีความเป็นตัวเลือก
  • commit ที่นำ --end-of-options เข้ามา ระบุว่าเพราะ -- เดิมถูกใช้แยกระหว่าง revision กับ pathspec อยู่แล้ว จึงต้องมีเครื่องหมายแยกอีกตัวสำหรับแยกตัวเลือกกับ revision
  • --end-of-options ถูกบันทึกไว้ในเอกสาร gitcli(7) และเพิ่มเข้ามาในเดือนพฤศจิกายน 2019 เมื่อ Git 2.24.0 ออก

วิธีใช้ที่ถูกต้องตามคำสั่ง

  • git clone -- "$url" เป็นไปตามธรรมเนียม POSIX ของ clone ดังนั้น -- หน้า URL จึงจบการตีความตัวเลือก
  • -- ด้านหลังใน git checkout "$ref" -- ระบุว่า $ref เป็น revision ไม่ใช่ชื่อไฟล์ แต่ไม่สามารถป้องกันไม่ให้ $ref ถูกตีความเป็นตัวเลือกก่อน
  • หากต้องการส่ง revision และ path ที่ไม่น่าเชื่อถือร่วมกันอย่างปลอดภัย ต้องใช้ เครื่องหมายทั้งสองตัว เช่น git log --end-of-options "$rev" -- "$path"
    • --end-of-options แยกตัวเลือกกับ revision
    • -- แยก revision กับ path
  • หากถือว่าสองเครื่องหมายนี้ใช้แทนกันได้ จะไม่สามารถบล็อกอินพุตที่ขึ้นต้นด้วยขีดได้

ช่วงเวลารองรับที่ต่างกันในแต่ละ subcommand

  • การรองรับ --end-of-options ไม่ได้ถูกนำไปใช้กับคำสั่ง Git ทั้งหมดพร้อมกัน แต่ถูก เพิ่มแยกตาม subcommand
  • git rev-parse ใช้ parser อาร์กิวเมนต์ของตัวเอง จึงเริ่มรองรับใน Git 2.30.0 ซึ่งช้ากว่าการเริ่มนำมาใช้ครั้งแรก 1 ปี
  • git checkout และ git reset ตีความ -- เอง และ implementation ช่วงแรกปล่อยให้ --end-of-options เหลืออยู่ในรายการอาร์กิวเมนต์จนคำสั่งปฏิเสธมัน
    • ปัญหานี้ได้รับการแก้ใน กุมภาพันธ์ 2024 เมื่อ Git 2.43.1 ออก

Argument injection ที่เกิดได้แม้ไม่ใช้ shell

  • Git, Mercurial และ SSH มีตัวเลือกที่เป็นฟีเจอร์ทางการสำหรับรันคำสั่งที่ผู้เรียกกำหนด
    • git clone --upload-pack=<cmd> ระบุไบนารีฝั่งเซิร์ฟเวอร์
    • -c core.sshCommand=<cmd> ในทุกการเรียก Git เปลี่ยนคำสั่งที่ใช้เชื่อมต่อ
    • --config=alias.<subcmd>=!<shell> ของ Mercurial นิยาม subcommand ที่จะรันใหม่เป็น shell script ใดก็ได้
    • -oProxyCommand=<cmd> ของ SSH ระบุคำสั่ง proxy
  • หากโปรแกรม wrapper ใส่สตริงที่ไม่น่าเชื่อถือเข้าไปในรายการอาร์กิวเมนต์ ฟีเจอร์เหล่านี้อาจกลายเป็น ช่องทางโจมตี
  • รูปแบบความล้มเหลวนี้เข้าข่าย CWE-88 argument injection และต่างจาก shell command injection
    • เกิดขึ้นได้แม้โปรแกรมใช้ argv array กับ exec แทน system()
    • array ถูกส่งต่อไปยัง Git โดยไม่เสียหาย แต่ Git ตีความอาร์กิวเมนต์ที่ขึ้นต้นด้วยขีดเป็นตัวเลือก
  • docker build CVE-2019-13139 เป็นกรณีที่ใช้ os/exec ของ Go และ argv array โดยไม่ผ่าน shell
    • ส่วน #ref:dir ของ Git context URL ถูกส่งต่อไปยัง git fetch origin <ref> ทำให้ <ref> ถูกตีความเป็น --upload-pack=<cmd>

ช่องโหว่รูปแบบเดิมที่เกิดซ้ำในระบบจัดการเวอร์ชันหลายตัว

  • ในวันเดียวกันของเดือนสิงหาคม 2017 มีการเปิดเผยรูปแบบเดียวกันในระบบจัดการเวอร์ชัน 4 ตัว
  • ทั้ง 4 ระบบส่ง hostname ใน URL เป็นอาร์กิวเมนต์ให้ SSH และ hostname ที่ขึ้นต้นด้วย -oProxyCommand= ถูกปฏิบัติเป็น ตัวเลือก SSH
  • ตาม postmortem ของ Phabricator ในบรรดาเครื่องมือ 3 ตัวที่ยังมีการดูแลอย่างแข็งขันในตอนนั้น มีเพียง Subversion ที่เพิ่ม -- หน้า hostname
    • Git และ Mercurial ตรวจสอบรูปแบบ hostname แทน ส่วนหนึ่งเพราะ SSH implementation บางตัวไม่รองรับ --
  • โค้ดที่ไม่มี -- ก็ดูเหมือนทำงานปกติจนกว่าอาร์กิวเมนต์จะเริ่มด้วยขีด ดังนั้นกลไกนี้จึง ไม่ปลอดภัยโดยพื้นฐาน

เส้นทางที่ package manager ถูกเปิดเผย

  • Package manager รับ Git URL หรือ ref จาก manifest, lock file และ metadata ของ transitive dependency แล้วส่งต่อให้ subprocess
    • gem 'foo', git: '...' ใน Gemfile
    • github:user/repo#ref ใน package.json
    • การตั้งค่าที่เทียบเท่าใน pyproject.toml, Cargo.toml, mix.exs, Package.swift, pubspec.yaml, conanfile.py, go.mod
  • จากการสำรวจ ณ HEAD เดือนกรกฎาคม 2026 ใน package manager 19 ตัว มี 17 ตัว ที่เรียกใช้ไบนารี Git เป็นเส้นทางหลักหรือเส้นทางเดียว
  • อีก 2 ตัวที่เหลือใช้ library เป็นค่าเริ่มต้น
  • Nix ใช้ libgit2 เมื่ออ่าน repository ในเครื่อง แต่รัน process ของ Git สำหรับ fetch เพราะ libgit2 ไม่รองรับ git-credential helper
  • กลุ่มที่สำรวจคือ Bundler, Cargo, CocoaPods, Composer, Conan, Go, Helm, Homebrew, Mix, Nix, npm, pip, pnpm, Poetry, Pub, SwiftPM, uv, vcpkg, Yarn

CVE ที่พบใน package manager

สถานะการป้องกันจริงและการแก้ของ Go

  • ใน package manager 17 ตัวที่เรียกใช้ process ของ Git มีเพียง cmd/go ของ Go ตัวเดียวที่ใช้ --end-of-options
  • Go เพิ่ม -- หน้า URL ของ repository ในเดือนมิถุนายน 2019 เพื่อ harden การป้องกันทั่วไป
  • ในเดือนมกราคม 2026 พบว่า -- เพียงอย่างเดียวไม่เพียงพอ จึง เพิ่ม --end-of-options ในวงกว้าง เป็นการแก้ CVE-2025-68119
  • การแก้เดียวกันยังรวม HGPLAIN=+strictflags
    • การตั้งค่านี้จำกัดการตีความตัวเลือกช่วงแรกของ Mercurial ตั้งแต่ปี 2017 เมื่อ Mercurial 4.4.2 ออก
  • commit แก้ของ Go ระบุว่าอาจต้องมีการเปลี่ยนแปลงเชิงโครงสร้างมากขึ้นเพื่อให้ปัญหาเดิมถูกนำกลับมาได้ยาก แต่ในขั้นแรกได้แก้ปัญหาปัจจุบันแล้ว

การป้องกันส่วนใหญ่ถูกเพิ่มหลังเปิดเผยช่องโหว่

  • Package manager ที่เหลือ แม้จะปกป้องรายการอาร์กิวเมนต์ ก็มักใช้ -- หรือ ปฏิเสธอินพุตที่ขึ้นต้นด้วยขีด
  • -- หน้า URL ของ git clone ใน Bundler ถูกเพิ่มใน แพตช์ CVE-2021-43809
  • การปฏิเสธขีดนำหน้าใน cocoapods-downloader ถูกนำเข้าใน 3 commit ช่วง 10 วันในเดือนมีนาคม 2022 ซึ่งสอดคล้องกับช่วงเปิดเผย CVE-2022-21223
  • การป้องกันของ Poetry เพิ่มในเดือนกันยายน 2021 และได้รับ CVE ในอีก 1 ปีถัดมา จากนั้นอีก 6 เดือนจึงเปลี่ยนไปใช้ dulwich
  • vcpkg เป็นข้อยกเว้น โดย ใช้ -- ตั้งแต่วันแรก ที่เขียนการรองรับ Git registry

ข้อจำกัดด้านความเข้ากันได้จากเวอร์ชัน Git ขั้นต่ำ

  • advisory ของ CVE-2022-24828 ของ Composer ระบุว่า --end-of-options คือการแก้ที่ถูกต้อง แต่เพราะต้องรองรับ Git ที่เก่ากว่า จึงเลือกปฏิเสธชื่อ branch ที่ขึ้นต้นด้วยขีดแทน
  • การผสาน Git ของ vcpkg ระบุเวอร์ชันขั้นต่ำเป็น Git 2.7.4 ส่วน HOMEBREW_MINIMUM_GIT_VERSION สำหรับ Linux ของ Homebrew คือ 2.7.0 ที่ตั้งไว้ในปี 2018
  • เมื่อ Amazon Linux 2 ที่ให้ Git 2.14.3 สิ้นสุดอายุในเดือนมิถุนายน 2026 distro ที่ขีดขั้นต่ำเหล่านี้ติดตามอยู่จึงเพิ่งหลุดจากขอบเขตรองรับ
  • สถานะ LTS ของ Ubuntu ก็ทำให้การเปลี่ยนพร้อมกันทำได้ยาก
    • Ubuntu 18.04 ให้ Git 2.17.0 และมี extended support ถึงปี 2028
    • Ubuntu 20.04 ให้ Git 2.25.1 และมี extended support ถึงปี 2030
    • Git 2.25.1 รับ --end-of-options ของ git fetch แต่ปฏิเสธใน git rev-parse
  • หากจะพึ่ง --end-of-options ต้องกำหนดเวอร์ชันขั้นต่ำเป็น Git 2.24.0 สำหรับ subcommand ส่วนใหญ่, 2.30.0 สำหรับ rev-parse, และ 2.43.1 สำหรับ checkout กับ reset
  • การยกเวอร์ชันขั้นต่ำจะทำให้ไม่สามารถรองรับผู้ใช้ที่ใช้ Git เก่าที่มากับ distro ได้

ใช้ Git library แทนการรัน process

  • libgit2, gitoxide, go-git, JGit, dulwich implement Git transport protocol ที่จำเป็นสำหรับ clone และ fetch ภายใน process
  • เมื่อไม่มีขอบเขต argv แยกต่างหาก จึง ไม่มีเป้าหมายให้ฉีดเข้าไปในรายการอาร์กิวเมนต์ตั้งแต่แรก
  • Jujutsu ใช้ gitoxide สำหรับการเชื่อมกับ Git และไม่มี CVE สาธารณะประเภท argument injection
    • advisory 2 รายการจนถึงตอนนี้คือ path traversal และการขาดการตรวจ SHA-1 collision ที่รับมาจาก library
  • CVE-2025-21613 ของ go-git จำกัดอยู่ที่ transport file://
    • เส้นทางนี้เป็น code path เดียวใน go-git ที่รันไบนารี Git
  • หากรวม Git implementation ของตัวเอง ต้องติดตามการแก้ด้านความปลอดภัย checkout ทั้งหมดที่ upstream Git ออกมา และทั้ง libgit2 กับ JGit เคยมีการแก้ในเรื่องนี้ซ้ำ ๆ
  • ต้นทุนนี้มีอยู่จริง แต่ปัญหาจะเปลี่ยนจากการต้องจำตรวจอาร์กิวเมนต์ถาวรในทุกจุดเรียก ไปเป็นการตาม กระแสแพตช์ upstream ที่เป็นรูปธรรม

ขอบเขตการเปลี่ยนแปลงที่เสนอให้ Homebrew

  • Homebrew PR ยกระดับเวอร์ชัน Git ขั้นต่ำเป็น 2.30.0 และเพิ่ม --end-of-options ในตำแหน่งต่อไปนี้
    • หน้า URL ของ clone, remote set-url, ls-remote
    • หน้า ref ของ rev-parse
  • ไม่เปลี่ยนการเรียก checkout และ reset
    • หากต้องการปกป้องทั้งสองคำสั่งนี้ด้วย ต้องใช้ Git 2.43.1 ที่ออกในเดือนกุมภาพันธ์ 2024
    • เวอร์ชันนี้ใหม่กว่า Git ที่ distro หลายตัวซึ่งยังได้รับการรองรับในปัจจุบันจัดให้

1 ความคิดเห็น

 
GN⁺ 6 시간 전
ความเห็นจาก Lobste.rs
  • ช่วงนี้ยิ่งใช้ jj มากขึ้นเรื่อย ๆ พอเทียบกับความรกของ Git แล้วก็สบายใจกว่ามาก แม้ยังอยู่ระหว่างการเรียนรู้ แต่ก็สะท้อนเจตนาได้ดีและเข้าใจวิธีทำงานที่ต้องการได้ง่าย
    โดยเฉพาะความรู้ที่ได้จากคำสั่ง jj หนึ่งคำสั่งสามารถนำไปใช้กับคำสั่งอื่นได้อย่างเป็นธรรมชาติ git log เอกสาร มีแฟล็กของตัวเลือกเอาต์พุตเพิ่มพูนจนล้น แถมยังปนกับ “สัญกรณ์พิเศษ” สำหรับไวยากรณ์ช่วงคอมมิตและแฟล็กกรองเพิ่มเติมอีกด้วย และยังมีความรู้ที่ถ่ายโอนไปยังคำสั่ง Git อื่นได้น้อย
    ตรงกันข้าม jj log เอกสาร กลับกระชับจนแม้แต่หน้าเดียวก็ยังเหลือที่ว่าง มันแทนที่ความซับซ้อนแบบจับฉ่ายของ Git ด้วย revision sets, file sets, output template DSL สามอย่าง และทั้งหมดนี้ถูกใช้อย่างสม่ำเสมอทั่วทั้ง jj จึงง่ายกว่า นำมาประกอบกันได้มากกว่า และเข้าใจได้ตรงไปตรงมามากกว่า แม้ต้องคำนึงว่า Git สะสมส่วนเกินมานาน 20 ปี แต่ jj ก็ดูได้เปรียบกว่ามากในการหลีกเลี่ยงปัญหานี้
  • รู้ดีว่าปกติในคำสั่งมักต้องมี -- นำหน้าอาร์กิวเมนต์ที่ผู้ใช้ป้อนเอง แต่ถ้าต้องมี กฎการแปลงเฉพาะของตัวเอง เพิ่มเข้ามาด้วย แบบนี้ก็กลายเป็นกับดักอันตรายเกินไป
  • นี่คือผลลัพธ์ที่ตรงไปตรงมาของการทำเครื่องมือให้ซับซ้อนเกินเหตุ Git ดูเหมือน บาชาโฮ ตัวใหม่
  • สงสัยว่าทำไมแต่แรกถึงเลือก -- มาใช้เพื่อแก้ความกำกวมของอาร์กิวเมนต์ไฟล์ แทนที่จะใช้วิธีง่าย ๆ แบบรับไฟล์เป็นอาร์กิวเมนต์ที่ชัดเจน มี การแลกเปลี่ยนเชิงออกแบบ อะไรที่ไม่เห็นจากภายนอกหรือเปล่า
    • การเอา ธรรมเนียมการพาร์สออปชันแบบ UNIX ที่มีอยู่แล้วมาใช้ในจุดประสงค์ที่ต่างออกไปโดยสิ้นเชิง เป็นความคิดที่โง่อย่างที่คาดไว้ และก็เป็นสไตล์ Git มาก ๆ
  • ปรัชญา ทุกอย่างคือข้อความ ของ UNIX ก่อปัญหาอีกแล้ว ทั้งที่บรรทัดคำสั่งเป็นตัวอย่างชั้นยอดของข้อมูลที่มีโครงสร้าง แต่ดูเหมือนต้นทุนในการประสานร่วมกันเพื่อหลุดจากหลุมนี้จะสูงเกินไป
    ถ้าย้อนกลับไปดูข้อถกเถียงระหว่าง Tcl กับ Scheme ในยุค 1990 สำหรับกรณีนี้อาจเป็นไปได้ว่า “ยิ่งแย่ยิ่งดีกว่า” นั้นถูกต้อง แม้ตระกูล S-expression รวมถึง JSON และ XML แทบจะปักหลักในระบบนิเวศ UNIX ไม่ได้เลย ขณะที่ Tcl มีแนวทางที่ค่อนข้างมีหลักการในการซ้อนภาษาย่อยไว้ภายในสตริง