สรุปสั้น

นักวิจัยของบริษัท Wiz เปิดเผยช่องโหว่ GitHub Actions workflow injection ตัวใหม่ในรีโปสาธารณะ snowflakedb/snowflake-connector-net ของบริษัท Snowflake ซึ่งระบุว่าถูกใช้โจมตีได้ด้วยการตั้ง GitHub issue ที่ปั้นขึ้นเป็นพิเศษ เพื่อสั่งรันคำสั่งใน workflow ที่ถือ credential ของ Jira ภายในองค์กรอยู่ ปัญหาอยู่ในไฟล์ .github/workflows/jira_issue.yml ที่ทำงานทุกครั้งเมื่อมีการเปิด issue สาธารณะ และเปิดเผยค่า JIRA_BASE_URL, JIRA_USER_EMAIL และ JIRA_API_TOKEN ให้กับขั้นตอนเดียวกันของ workflow นั้น จุดสำคัญคือความเสียหายจำกัดอยู่ที่ระบบอัตโนมัติ CI/CD ของรีโป และไม่พบว่ามีเวอร์ชันของ Snowflake Connector for .NET ที่ได้รับผลกระทบ

ต้นเหตุมีสองชั้น ชั้นแรกคือ workflow เอาค่าหัวเรื่องและเนื้อความของ issue ซึ่งผู้โจมตีควบคุมได้ ใส่ตรงเข้าไปในบล็อก run: ที่เป็นเชลล์ ชั้นที่สองคือการตรวจเงื่อนไขผู้ส่งด้วย github.event.pull_request.user.login แม้อีเวนต์ที่เกิดขึ้นจะเป็น issue ไม่ใช่ pull request ทำให้พร็อพเพอร์ตี้ที่อ้างถึงนั้นไม่มีอยู่จริง GitHub ระบุว่าการอ้างถึงพร็อพเพอร์ตี้ที่ไม่มีอยู่จะถูกประเมินค่าเป็นสตริงว่าง ผลคือการเทียบกับ whitesource-for-github-com[bot] ไม่ได้หยุด issue ธรรมดาไม่ให้เข้าถึงจ็อบนั้นเลย

Wiz แจ้งปัญหาไปยัง Snowflake ผ่าน HackerOne เมื่อวันที่ 23 มิถุนายน 2569 และ Snowflake รวมแพตช์ในวันเดียวกัน ขณะที่ workflow ที่มีช่องโหว่เพิ่งขึ้นสู่ default branch ก่อนหน้านั้นเพียงห้าวัน กรอบเวลาการเปิดรับความเสี่ยงจึงสั้น และ ณ วันที่ 17 สิงหาคม 2569 ยังไม่พบว่ามีการออกหมายเลข CVE คะแนน CVSS หรือรายการในแคตตาล็อก Known Exploited Vulnerabilities (KEV) ของ CISA สำหรับกรณีนี้

รายละเอียดข่าว

เว็บไซต์ The Hacker News รายงานเมื่อวันที่ 18 สิงหาคม 2569 ว่านักวิจัยความปลอดภัยของบริษัท Wiz เปิดเผยช่องโหว่ GitHub Actions workflow injection ในรีโปสาธารณะของ Snowflake ที่ชื่อ snowflakedb/snowflake-connector-net ซึ่งอาจถูกใช้ประโยชน์ผ่าน GitHub issue ที่ปั้นขึ้นเป็นพิเศษ เพื่อรันคำสั่งภายใน workflow ที่มี credential ของ Jira ภายในองค์กรอยู่

Wiz ระบุว่าระบบชื่อ Red Agent ของตนใช้ประโยชน์จากช่องโหว่นี้ได้จริงในการทดสอบความปลอดภัยที่ได้รับอนุญาต โดยเพย์โหลดชุดแรกทำให้เกิด shell syntax error ก่อน แล้วระบบจึงเปลี่ยนวิธี หลังจากนั้นนักวิจัยได้รับ callback แบบ out-of-band จาก GitHub Actions runner และได้โทเคน Jira API ที่ workflow นั้นใช้อยู่

ตามข้อมูลของ Wiz โทเคนดังกล่าวเป็นของบัญชี qa@snowflake.net และให้สิทธิ์อ่านโปรเจกต์ Jira ที่ครอบคลุมงานด้านวิศวกรรม การกำกับดูแลความปลอดภัย และการติดตามงาน bug bounty บนโดเมน snowflakecomputing.atlassian.net ทั้งนี้ Wiz ระบุว่าสิทธิ์ที่แท้จริงของ Jira รวมถึงบันทึกการรัน workflow และบันทึกการตรวจสอบไม่ได้เปิดเผยต่อสาธารณะ

GitHub Actions workflow injection in a public Snowflake repository lets a crafted issue run commands and leak internal Jira credentials

ไทม์ไลน์ของเรื่องนี้แน่นมาก Wiz แจ้งปัญหาไปยัง Snowflake ผ่านแพลตฟอร์ม HackerOne เมื่อวันที่ 23 มิถุนายน 2569 ในรายงานหมายเลข #3819931 และ Snowflake รวมการแก้ไขในวันเดียวกันผ่าน pull request #1402 ด้วยการเปลี่ยนจากการขยายค่า GitHub expression โดยตรง ไปเป็นการส่งค่าผ่านตัวแปรสภาพแวดล้อมแล้วส่งต่อให้ jq ในรูปของอาร์กิวเมนต์ ส่วน workflow ที่มีช่องโหว่ขึ้นสู่ default branch ก่อนหน้านั้นห้าวัน คือวันที่ 18 มิถุนายน เมื่อ pull request #1218 ถูก merge และการจัดการค่าที่แก้ไขแล้วยังคงอยู่ในสาขา master ของรีโปจนถึงปัจจุบัน

Snowflake ให้ถ้อยแถลงซึ่ง Wiz นำมาเผยแพร่ว่า “การตรวจสอบของเราไม่พบหลักฐานของการเข้าถึงโดยไม่ได้รับอนุญาต” ขณะที่ Wiz ระบุว่าโทเคน Jira ถูกหมุนเปลี่ยนเมื่อวันที่ 24 มิถุนายน และการทบทวนของ Snowflake ไม่พบการใช้โทเคนนั้นจากภายนอกที่ไม่เกี่ยวข้องกันในช่วงห้าวันที่เปิดรับความเสี่ยง อย่างไรก็ตาม Wiz ย้ำว่าบันทึกการตรวจสอบที่เป็นฐานของข้อสรุปนั้นไม่ได้เผยแพร่ต่อสาธารณะ

อีกประเด็นที่ต้องอ่านอย่างระวังคือเรื่องที่มาของโค้ด Wiz อธิบายว่าช่องโหว่เกิดจากการเปลี่ยนแปลงโดย GitHub Copilot Autofix แต่ประวัติของ GitHub เองไม่ได้ยืนยันว่า Copilot เป็นผู้เขียนโค้ดใน jira_issue.yml ที่มีช่องโหว่ เพราะคอมมิตที่ระบุ Copilot เป็นผู้ร่วมเขียนอย่างชัดเจนคือ 6d0e2fa ซึ่งไปแก้ไฟล์ jira_close.yml ขณะที่การรีแฟกเตอร์ jira_issue.yml ที่ไม่ปลอดภัยปรากฏในคอมมิตแยกอีกตัวลงวันที่ 25 สิงหาคม 2568 คือ 094038e ซึ่ง GitHub ระบุผู้เขียนเป็น sfc-gh-hpathak การเปลี่ยนแปลงทั้งสองถูกยุบรวมเข้าคอมมิต squash merge 4a1b8ce ของวันที่ 18 มิถุนายน ซึ่งมีชื่อ Copilot Autofix อยู่ในรายการผู้ร่วมเขียน ประวัติคอมมิตจึงยืนยันได้เพียงว่า Copilot มีส่วนร่วมใน pull request #1218 แต่ไม่ได้ยืนยันว่าเป็นผู้เขียนบรรทัดที่มีช่องโหว่

ทั้งนี้ GitHub เคยจัดทำเอกสารเตือนถึงช่องโหว่ประเภทนี้ไว้แล้วเมื่อเดือนกรกฎาคม 2568 โดยเตือนไม่ให้ขยายข้อมูล issue ที่ไม่น่าเชื่อถือเข้าไปในบล็อก run: โดยตรง และแนะนำให้ใช้ตัวแปรสภาพแวดล้อมเป็นตัวกลาง ส่วน ณ วันที่ 17 สิงหาคม 2569 ยังไม่พบว่ามีการออกหมายเลข CVE คะแนน CVSS หรือรายการ KEV สำหรับกรณีนี้ ไม่พบการอัปเดตเวอร์ชันของตัว connector ที่ผูกกับเรื่องนี้ ค่าการแทรกที่ไม่ปลอดภัยไม่ปรากฏบนสาขา master อีกแล้ว และเอกสารต้นทางที่มีอยู่ไม่ได้ยืนยันว่ามีการใช้ประโยชน์ในทางร้ายจริงหรือมีลูกค้ารายใดถูกเจาะ

วิธีการโจมตี

หัวใจของ workflow injection คือการที่ระบบอัตโนมัติเชื่อข้อมูลที่คนนอกพิมพ์เข้ามา jira_issue.yml ถูกตั้งให้ทำงานเมื่อมีการเปิด issue สาธารณะ ซึ่งหมายความว่าใครก็เรียกให้มันรันได้ แล้ว workflow นำค่าหัวเรื่องและเนื้อความของ issue ไปวางลงในบล็อก run: ตรง ๆ บล็อกนั้นคือคำสั่งเชลล์ ดังนั้นข้อความที่ผู้โจมตีเขียนจึงถูกตีความเป็นส่วนของคำสั่ง ไม่ใช่ข้อมูล และในขั้นตอนเดียวกันนั้นเองมีการส่ง JIRA_BASE_URL, JIRA_USER_EMAIL และ JIRA_API_TOKEN เข้ามาให้ใช้งาน เท่ากับความลับกับข้อความของคนแปลกหน้าอยู่ในบริบทเดียวกัน

ประตูที่ควรกันไว้กลับไม่ทำงานเพราะเงื่อนไขเขียนผิดประเภทอีเวนต์ ผู้พัฒนาตั้งเงื่อนไขตรวจว่าผู้ส่งคือบอต whitesource-for-github-com[bot] หรือไม่ แต่ไปอ่านค่าจาก github.event.pull_request.user.login ขณะที่อีเวนต์ที่กระตุ้น workflow เป็น issue ไม่ใช่ pull request เมื่อพร็อพเพอร์ตี้นั้นไม่มีอยู่ ค่าที่ได้จึงเป็นสตริงว่างตามที่ GitHub อธิบายไว้ว่า “หากคุณพยายามอ้างถึงพร็อพเพอร์ตี้ที่ไม่มีอยู่ ค่าจะถูกประเมินเป็นสตริงว่าง” การเทียบค่ากับชื่อบอตจึงไม่มีผลกันใครออกไปได้จริง และ issue ธรรมดาจากบัญชีใดก็เดินเข้าไปถึงจ็อบได้

ในการทดสอบ Wiz เล่าว่าเพย์โหลดแรกทำให้เกิด shell syntax error ซึ่งเป็นสัญญาณคลาสสิกว่าข้อความถูกตีความเป็นคำสั่งจริง ระบบ Red Agent จึงปรับวิธีเขียนเพย์โหลดใหม่ แล้วได้รับการติดต่อกลับแบบ out-of-band จาก runner ของ GitHub Actions ซึ่งเป็นการยืนยันว่ารันคำสั่งได้สำเร็จ และดึงโทเคน Jira API ออกมาได้จากสภาพแวดล้อมของขั้นตอนนั้น การแก้ไขของ Snowflake จึงตรงกับคำแนะนำมาตรฐาน คือเลิกขยาย GitHub expression ลงในเชลล์โดยตรง แล้วส่งค่าผ่านตัวแปรสภาพแวดล้อมและป้อนให้ jq เป็นอาร์กิวเมนต์แทน

ผลกระทบ

สิ่งที่กรณีนี้แสดงให้เห็นคือพื้นผิวการโจมตีที่ไม่ได้อยู่ในซอฟต์แวร์ที่ส่งมอบให้ลูกค้า แต่อยู่ในระบบอัตโนมัติรอบ ๆ รีโปสาธารณะ ซึ่งหลายองค์กรมองว่าเป็นงานบ้าน ไม่ใช่ระบบที่ถือความลับ ทั้งที่ workflow เหล่านี้มักได้รับ secret เพื่อคุยกับ Jira, Slack หรือคลังอาร์ติแฟกต์ภายใน การที่คนภายนอกเปิด issue หนึ่งใบแล้วได้โทเคนที่อ่านโปรเจกต์วิศวกรรมและงาน bug bounty ได้ หมายความว่าผู้โจมตีอาจเห็นรายการช่องโหว่ที่ยังไม่ถูกแก้และรายละเอียดการสืบสวนภายใน ซึ่งเป็นข้อมูลที่มีค่ากว่าตัวโทเคนเสียเอง

ในทางกลับกัน ขอบเขตของเรื่องนี้แคบกว่าที่พาดหัวชวนให้คิด เพราะปัญหาอยู่ในไฟล์ workflow ของรีโปเดียว ไม่ใช่ในตัว Snowflake Connector for .NET ที่ลูกค้านำไปใช้ ไม่มีเวอร์ชันของ connector ที่ต้องอัปเดตเพราะเรื่องนี้ และช่วงเวลาที่ workflow ที่มีช่องโหว่อยู่บน default branch คือห้าวัน ผู้ใช้ connector จึงไม่มีอะไรต้องแพตช์ ส่วนสิ่งที่ควรทำคือย้อนดูว่าองค์กรของตัวเองมี workflow ที่ทำงานตามอีเวนต์จากคนนอกและถือ secret อยู่ในขั้นตอนเดียวกันหรือไม่

ข้อจำกัดของหลักฐานมีสองจุดที่ควรระบุไว้ตรง ๆ จุดแรกคือคำยืนยันว่า “ไม่พบหลักฐานการเข้าถึงโดยไม่ได้รับอนุญาต” มาจาก Snowflake เอง และบันทึกการตรวจสอบที่เป็นฐานของข้อสรุปนั้นไม่ได้เปิดเผย จึงเป็นการยืนยันที่ตรวจสอบจากภายนอกไม่ได้ จุดที่สองคือการโยงเรื่องเข้ากับ Copilot Autofix ซึ่งเป็นการตีความของ Wiz ขณะที่ประวัติคอมมิตชี้ว่าบรรทัดที่ไม่ปลอดภัยมาจากคอมมิตที่ระบุผู้เขียนเป็นวิศวกรของ Snowflake เมื่อเดือนสิงหาคม 2568 ประเด็น “AI เขียนโค้ดที่มีช่องโหว่” จึงยังไม่ใช่ข้อสรุปที่หลักฐานสาธารณะรองรับในกรณีนี้ นอกจากนี้ยังไม่มีการยืนยันว่ามีการใช้ประโยชน์ในทางร้ายจริง

คำแนะนำ

  • ห้ามนำค่าจากอีเวนต์ที่คนนอกควบคุมได้ เช่น github.event.issue.title หรือ github.event.issue.body ไปขยายในบล็อก run: โดยตรง ให้ส่งผ่านตัวแปรสภาพแวดล้อมแล้วอ้างอิงเป็นตัวแปรของเชลล์หรืออาร์กิวเมนต์ของโปรแกรมเสมอ
  • ไล่ตรวจ workflow ทุกไฟล์ที่ทริกเกอร์ด้วย issues, issue_comment, pull_request_target หรือ workflow_run ในรีโปสาธารณะ แล้วดูว่าขั้นตอนใดได้รับ secret ควบคู่กับข้อมูลจากผู้ใช้ภายนอก
  • ตรวจว่าเงื่อนไขกรองผู้ส่งอ่านค่าจากพร็อพเพอร์ตี้ที่มีอยู่จริงในอีเวนต์นั้น เพราะพร็อพเพอร์ตี้ที่ไม่มีอยู่จะกลายเป็นสตริงว่างและทำให้การเทียบค่าผ่านไปเงียบ ๆ วิธีที่ปลอดภัยกว่าคือกรองที่ระดับ on: และแยกจ็อบที่ต้องใช้ secret ออกจากจ็อบที่รับข้อมูลจากภายนอก
  • จำกัดสิทธิ์ของ credential ที่ workflow ใช้ให้แคบที่สุดเท่าที่งานนั้นต้องการ โทเคนที่ใช้เพียงสร้างตั๋ว Jira ไม่ควรอ่านโปรเจกต์ด้านความปลอดภัยหรือ bug bounty ได้
  • ตั้งรอบหมุน secret ของ CI/CD และเก็บบันทึกการรัน workflow ไว้ให้ตรวจย้อนหลังได้ เพื่อให้การประเมินหลังเหตุการณ์ไม่ต้องพึ่งคำยืนยันเพียงคำเดียว
  • ตรวจโค้ดที่เครื่องมืออัตโนมัติหรือผู้ช่วย AI เสนอเข้ามาในไฟล์ workflow ด้วยมาตรฐานเดียวกับโค้ดของระบบจริง โดยเฉพาะเมื่อ pull request เป็นแบบ squash merge ที่รวมการเปลี่ยนแปลงหลายชุดจากหลายผู้เขียนไว้ในคอมมิตเดียว

แหล่งอ้างอิง