ลูปแพตช์เชิงปฏิปักษ์ตัดสินอย่างไรว่าช่องโหว่ถูกปิดผนึกจริง
เครื่องมือความปลอดภัยส่วนใหญ่หยุดอยู่ที่ “ข้อเสนอการแก้ไข” คำถามถัดไป — แพตช์นี้หยุดการโจมตีได้จริงหรือไม่ — ถูกโยนกลับให้มนุษย์ ลูปแพตช์เชิงปฏิปักษ์เกิดขึ้นเพื่อตอบคำถามนั้น และคำตอบไม่ใช่แค่ผ่านหรือไม่ผ่าน แต่เป็นหลักฐานที่ทำซ้ำได้
โดย ทีมวิศวกรรมความปลอดภัย FloatFactory
เอนจิน FlawDetector · งานวิจัย
การสแกนซ้ำไม่ใช่ข้อพิสูจน์
เวิร์กโฟลว์มาตรฐานของการวิเคราะห์แบบสถิตมักจบแบบนี้: พบข้อบกพร่อง เสนอการแก้ไข นักพัฒนาเมิร์จแพตช์ สแกนรอบถัดไปยืนยันว่าข้อบกพร่องหายไป แล้วปิดทิกเก็ต ปัญหาคือขั้นตอนสุดท้ายไม่ได้พิสูจน์อะไรเลย การตัดสินที่สร้างข้อบกพร่องกับการตัดสินที่ประกาศว่ามันหายไป มาจากเอนจินเดียวกัน กฎชุดเดียวกัน และมุมมองเดียวกัน เท่ากับคนออกข้อสอบตรวจคำตอบของตัวเอง
ในรีโพซิทอรีของลูกค้า เราพบรูปแบบความล้มเหลวซ้ำ ๆ สามแบบ แบบแรกคือ การเลี่ยงลายเซ็น แพตช์เปลี่ยนเฉพาะรูปแบบสตริงที่กฎจับได้ สแกนเนอร์จึงเงียบลงทั้งที่เส้นทางโจมตียังเปิดอยู่ แบบที่สองคือ การบรรเทาบางส่วน ปิดเฉพาะทางเข้าที่เห็นชัดที่สุด แล้วปล่อยแฮนด์เลอร์ตัวที่สองซึ่งมีจุดอ่อนเดียวกันไว้ตามเดิม แบบที่สามคือ การย้ายตำแหน่ง ตรรกะตรวจสอบถูกยกขึ้นไปชั้นบน แล้วเปิดช่องอ้อมใหม่ระหว่างทาง ทั้งสามกรณีนี้ผลสแกนซ้ำออกมาสะอาดหมด
สแกนเนอร์เงียบลงกับการโจมตีล้มเหลวเป็นคนละเหตุการณ์กัน ไปป์ไลน์ส่วนใหญ่วัดอย่างแรกแล้วรายงานอย่างหลัง
ห้าเฟสของหนึ่งรอบ
ลูปแพตช์เชิงปฏิปักษ์ให้เอเจนต์สองตัวปะทะกันภายใน CI โดย AI เรดทีมรับข้อบกพร่องที่ยืนยันแล้วหนึ่งรายการแล้วพยายามเจาะจริง ส่วน AI บลูทีมเขียนแพตช์ที่เล็กที่สุดซึ่งทำให้ความพยายามนั้นล้มเหลว หนึ่งรอบประกอบด้วยห้าเฟส และแต่ละเฟสทิ้งผลผลิตไว้ให้เฟสถัดไปใช้
- 01
โจมตี — เรดทีมวางแผนเอ็กซ์พลอยต์
รับตำแหน่งข้อบกพร่อง กราฟการเรียก และรายการจุดเข้า แล้วประกอบลำดับการโจมตีหลายขั้น ผลผลิตคือลำดับคำขอที่รันได้จริง พร้อมเงื่อนไขสังเกตที่ใช้ตัดสินความสำเร็จ ได้แก่ เนื้อหาการตอบกลับ สถานะโค้ด และผลข้างเคียง
- 02
ตรวจจับการเจาะ — รันในสภาพแวดล้อมแยก
สร้างรีโพจากคอมมิตที่ทดสอบลงคอนเทนเนอร์ใช้แล้วทิ้งแล้วรันลำดับนั้น เมื่อเงื่อนไขสังเกตถูกเติมเต็ม ข้อบกพร่องจะเลื่อนสถานะจาก “ความเสี่ยงเชิงทฤษฎี” เป็น “เอ็กซ์พลอยต์ที่ทำซ้ำได้” พร้อมบันทึกร่องรอยไว้
- 03
สร้างแพตช์ — บลูทีมเปลี่ยนให้น้อยที่สุด
อินพุตของบลูทีมคือร่องรอยเอ็กซ์พลอยต์ ไม่ใช่โค้ดที่บกพร่อง เป้าหมายคือการเปลี่ยนแปลงที่เล็กที่สุดซึ่งทำให้ร่องรอยนี้ล้มเหลว ส่วนการรีแฟกเตอร์และการปรับสไตล์ถูกกันออกอย่างชัดเจน
- 04
โจมตีซ้ำ — ยิงทั้งของเดิมและของกลายพันธุ์
รีเพลย์ลำดับต้นฉบับกับทรีที่แพตช์แล้ว จากนั้นยิงชุดกลายพันธุ์ตาม หากยังมีอันใดทะลุ ผลนั้นจะกลายเป็นอินพุตของบลูทีมในรอบถัดไป
- 05
ยืนยันการปิดผนึก — ตรวจว่าไม่มีอะไรพัง
แพตช์ที่ผ่านด่านความปลอดภัยยังต้องผ่านชุดทดสอบเดิมและการเทียบการตอบกลับกับอินพุตปกติ แพตช์ที่หยุดการโจมตีด้วยการทำให้ฟีเจอร์พังไม่นับว่าปิดผนึก
ด่านสามชั้นของลูปแพตช์เชิงปฏิปักษ์
คำว่า “ปิดผนึก” ที่นี่ไม่ใช่ถ้อยคำการตลาด แต่เป็นป้ายที่ติดได้ก็ต่อเมื่อเงื่อนไขบูลีนสามข้อเป็นจริงพร้อมกัน หากข้อใดเป็นเท็จ สถานะจะไม่ใช่ปิดผนึก และบันทึกการตัดสินจะระบุว่าติดที่ด่านใดเพราะอะไร
| ด่าน | เงื่อนไขผ่าน | เมื่อไม่ผ่าน |
|---|---|---|
| เอ็กซ์พลอยต์ต้นฉบับ | ลำดับที่สำเร็จในรอบแรกถูกรีเพลย์สามครั้งและล้มเหลวทุกครั้ง | แพตช์ไม่ได้แตะเส้นทางโจมตี ส่งร่องรอยกลับให้บลูทีมเขียนใหม่ |
| ชุดกลายพันธุ์ | การกลายพันธุ์ 12 รายการจากสามตระกูล — การเข้ารหัส การย้ายบริบท และการเรียงลูกโซ่ใหม่ — ล้มเหลวทั้งหมด | เป็นแพตช์ที่กันเฉพาะลายเซ็น นำการกลายพันธุ์ที่ทะลุใส่บริบทเข้ารอบถัดไป |
| การถดถอยและความเท่าเทียมเชิงพฤติกรรม | ชุดทดสอบเดิมผ่านทั้งหมด และการตอบกลับต่ออินพุตปกติเหมือนก่อนแพตช์ | ปลอดภัยแต่ฟีเจอร์พัง ต้องลดขอบเขตการเปลี่ยนแปลงแล้วลองใหม่ |
ทั้งสามด่านถูกประเมินตามลำดับ หากด่านก่อนหน้าไม่ผ่าน ด่านถัดไปจะไม่ถูกรัน
การกลายพันธุ์ถูกสร้างขึ้นอย่างไร
ชุดกลายพันธุ์มีหน้าที่เดียว คือ บอกว่าบลูทีมกันสตริงหรือกันเส้นทาง โดยใช้ร่องรอยเอ็กซ์พลอยต์ที่สำเร็จเป็นเมล็ดพันธุ์ แล้วแตกออกเป็นสามตระกูล
- ตระกูลการเข้ารหัส — URL encoding ซ้อนสองชั้น ความต่างของ Unicode normalization การแทรก null byte และการสลับตัวพิมพ์ ทั้งหมดคือความหมายเดิมที่แสดงด้วยไบต์ต่างออกไป ตัวกรองที่อิงเรกูลาร์เอ็กซ์เพรสชันมักพังตรงนี้
- ตระกูลการย้ายบริบท — เพย์โหลดเดิมถูกส่งผ่านจุดเข้าอื่น เช่น เราต์อื่น batch API ตัวรับ webhook หรือแฮนด์เลอร์ที่เปิดเฉพาะผู้ดูแลระบบ นี่คือด่านที่จับการบรรเทาบางส่วน
- ตระกูลการเรียงลูกโซ่ใหม่ — สลับลำดับของการโจมตีหลายขั้น หรือแทนขั้นกลางด้วยวิธีอื่น เช่น เปลี่ยนลูกโซ่ auth bypass → อ่านไฟล์ ให้ยาวขึ้นเป็น auth bypass → path traversal → อ่านไฟล์
ค่าเริ่มต้นคือ 12 การกลายพันธุ์ โดยถ่วงน้ำหนักตามชนิดข้อบกพร่อง กลุ่ม injection จะหนักไปทางตระกูลการเข้ารหัส ส่วนข้อบกพร่องด้านสิทธิ์มีการย้ายบริบทเกินครึ่ง การสร้างเป็นแบบกำหนดผลได้ภายใต้ seed ที่ตรึงไว้ ข้อบกพร่องเดียวกันจึงสร้างชุดเดิมได้เสมอ เพราะในการตรวจสอบภายหลังต้องแสดงให้เห็นได้ว่าตอนนั้นยิงอะไรไปบ้าง
เมื่อปิดผนึกไม่ได้ — บรรเทาและยกระดับ
ลูปไม่ได้วนไม่รู้จบ เพดานเริ่มต้นคือสี่รอบ และในแต่ละรอบบลูทีมจะรับบริบทเต็มของความล้มเหลวครั้งก่อนไปด้วย เมื่อถึงเพดาน ข้อบกพร่องจะลงเอยที่หนึ่งในสามสถานะปลายทาง
- sealed (ปิดผนึก) — ผ่านครบทั้งสามด่าน diff ของแพตช์และบันทึกการตัดสินจะถูกเปิดเป็น pull request
- mitigated (บรรเทาแล้ว) — การโจมตีต้นฉบับล้มเหลว แต่การกลายพันธุ์บางรายการยังทะลุ ขั้นตอนการทำซ้ำของรายการที่รอดจะถูกแนบไว้ครบ
- escalated (ยกระดับ) — ข้อบกพร่องเชิงโครงสร้างที่แพตช์เล็กที่สุดแก้ไม่ได้ ถูกส่งต่อให้มนุษย์พร้อมข้อสรุปว่าต้องเปลี่ยนการออกแบบ ตัวอย่างคลาสสิกคือโมเดลการให้สิทธิ์ที่ผิดตั้งแต่ต้น
2.3
จำนวนรอบเฉลี่ยจนปิดผนึก
ข้อบกพร่องระดับวิกฤตและสูง มัธยฐาน 2 รอบ
88.4%
ปิดผนึกได้ภายในสี่รอบ
จากข้อบกพร่องที่เข้าลูป
8.1%
ปิดในสถานะบรรเทา
ความเสี่ยงลดลงแต่ยังมีการกลายพันธุ์รอด
3.5%
ยกระดับให้มนุษย์
ส่วนใหญ่เป็นเรื่องการออกแบบและโมเดลสิทธิ์
วิธีอ่านบันทึกการตัดสิน
ทุกลูปจบลงด้วยอ็อบเจ็กต์การตัดสินหนึ่งชุด อ็อบเจ็กต์เดียวกันนี้ถูกใช้ทั้งในแดชบอร์ด เกตการเมิร์จ และการสร้างรายงานสำหรับการตรวจสอบ
{
"finding": "FD-2026-07-1183",
"cwe": "CWE-89",
"severity": "critical",
"verdict": "sealed",
"rounds": 2,
"gates": {
"original_exploit": { "reproduced": false, "attempts": 3 },
"mutation_suite": {
"passed": 12,
"failed": 0,
"families": ["encoding", "context-shift", "chain-reorder"],
"seed": "fd-1183-a"
},
"regression": { "tests": 412, "failed": 0, "behavioural_diff": "none" }
},
"patch": { "commit": "a1f7c02", "files": 1, "added": 6, "removed": 2 },
"sealed_at": "2026-07-09T04:12:38Z"
}ฟิลด์ที่สำคัญที่สุดไม่ใช่ verdict แต่เป็น gates เพราะ verdict บอกแค่ผ่านหรือไม่ผ่าน ส่วน gates เผยให้เห็นว่าการตัดสินนั้นอิงอะไร และเนื่องจากมีการบันทึก mutation_suite.seed ไว้ ผู้ตรวจสอบในอีกหกเดือนข้างหน้าจึงสร้างชุดกลายพันธุ์เดิมขึ้นมาใหม่และยืนยันผลเดียวกันได้
ต่อเข้ากับ CI ในฐานะเกต
คุณค่าที่แท้จริงของลูปปรากฏเมื่อใช้เป็นเกตการเมิร์จ การตั้งค่าที่พบบ่อยคือกำหนดสถานะปลายทางต่างกันตามระดับความรุนแรง ระดับวิกฤตและสูงต้องถึงสถานะ sealed ส่วนระดับกลางลงมายอมรับ mitigated ได้
name: security-gate
on: [pull_request]
jobs:
flawdetector:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: floatfactory/flawdetector-action@v1
with:
api-key: ${{ secrets.FLAWDETECTOR_API_KEY }}
mode: loop # สแกนแล้วรันลูปเรดทีม ปะทะ บลูทีมต่อ
max-rounds: 4
seal-gate: critical,high # ระดับเหล่านี้ต้องถึง sealed จึงจะเมิร์จได้
allow-mitigated: medium,low
comment-pr: true # โพสต์บันทึกการตัดสินลงคอมเมนต์ PR
# exit code
# 0 ผ่านเกต
# 12 ยังมีข้อบกพร่อง mitigated ในระดับที่ตั้ง seal-gate ไว้
# 13 มีข้อบกพร่อง escalated — ต้องให้มนุษย์รีวิวเมื่อเปิดเกตครั้งแรก แนะนำให้เริ่มที่ seal-gate: critical การใส่ระดับสูงเข้าไปพร้อมกันมักทำให้หนี้เดิมบล็อกทุกการเมิร์จตั้งแต่วันแรก และการตัดสินใจที่ตามมามักคือปิดเกตทิ้ง การแยกหนี้ออกเป็นแบ็กล็อกต่างหากแล้วให้เกตทำงานเฉพาะโค้ดใหม่ มีอัตราอยู่รอดสูงกว่ามาก
เราวัดอะไร และไม่เชื่ออะไร
ตัวชี้วัดที่เราติดตามภายในไม่ใช่ “จำนวนที่แก้อัตโนมัติได้” เพราะตัวเลขนั้นโตตามความรกของโค้ดเบส จึงสัมพันธ์กับคุณภาพน้อย เราดูสามตัวนี้แทน
- เวลานำจนปิดผนึก — จากข้อบกพร่องที่ยืนยันแล้วจนถึงสถานะ sealed เป็นตัวชี้วัดความเร็วเดียวที่ทีมรู้สึกได้จริง
- อัตราการเปิดซ้ำ — สัดส่วนของข้อบกพร่องที่ปิดผนึกแล้วแต่ทำซ้ำได้อีกภายใน 90 วัน มันจะขยับก่อนใครเมื่อเกณฑ์ตัดสินเริ่มหลวม
- อัตราการทะลุของชุดกลายพันธุ์ — ความถี่ที่แพตช์รอบแรกถูกชุดกลายพันธุ์จับได้ ใช้เป็นตัวแทนของคำถามว่าบลูทีมกำลังกันสตริงหรือกันเส้นทาง
ตัวชี้วัดที่เราไม่เชื่อก็ชัดเจนพอกัน อัตราการยอมรับแพตช์ สูงขึ้นได้แม้นักพัฒนาจะกดอนุมัติโดยไม่อ่าน diff และ ค่าเฉลี่ยการลดความรุนแรง ดีขึ้นได้เพียงแค่เปลี่ยนนโยบายการติดป้าย สำหรับเครื่องมือความปลอดภัย คำถามแรกของทุกตัวชี้วัดคือเครื่องมือนั้นขยับมันเองได้หรือไม่
สรุป
หลักการออกแบบเบื้องหลังลูปสรุปได้ในประโยคเดียว คือ แยกฝ่ายที่ตัดสินออกจากฝ่ายที่แก้ และเก็บฐานของการตัดสินให้ทำซ้ำได้ การแยกเรดทีมกับบลูทีม การตรึง seed ของชุดกลายพันธุ์ และการคงป้าย sealed กับ mitigated ให้ต่างกัน ล้วนมาจากหลักการเดียวกันนี้
ส่วนที่ยากของการทำระบบอัตโนมัติด้านความปลอดภัยไม่เคยเป็นการเขียนโค้ดแก้ไข แต่คือการไม่ตรวจคำตอบของตัวเอง และการพูดออกมาตรง ๆ เมื่อการแก้ไขนั้นยังไม่พอ
คำถามที่พบบ่อย
- ลูปแพตช์เชิงปฏิปักษ์โจมตีระบบที่ให้บริการจริงหรือไม่
- ไม่ ความพยายามเจาะทุกครั้งรันอยู่ในคอนเทนเนอร์ใช้แล้วทิ้งที่สร้างจากคอมมิตที่กำลังทดสอบ และปิดการเชื่อมต่อออกภายนอก ไม่มีคำขอใดไปถึงสภาพแวดล้อมโปรดักชันหรือสเตจจิง
- ปิดผนึกกับบรรเทาแล้วต่างกันอย่างไร
- ปิดผนึกหมายถึงผ่านครบสามด่าน คือเอ็กซ์พลอยต์ต้นฉบับทำซ้ำไม่ได้ การกลายพันธุ์ทั้ง 12 รายการล้มเหลว และการถดถอยกับความเท่าเทียมเชิงพฤติกรรมผ่าน ส่วนบรรเทาแล้วหมายถึงการโจมตีต้นฉบับถูกกันได้ แต่ยังมีการกลายพันธุ์อย่างน้อยหนึ่งรายการทะลุ ความเสี่ยงลดลงแต่เส้นทางโจมตียังไม่ปิด
- ควรเมิร์จแพตช์ที่ลูปสร้างโดยอัตโนมัติหรือไม่
- เราไม่แนะนำ พฤติกรรมเริ่มต้นคือเปิด pull request ที่มีทั้ง diff และบันทึกการตัดสิน แล้วให้มนุษย์อนุมัติ แพตช์ที่ได้สถานะ sealed ผ่านการตรวจการถดถอยและความเท่าเทียมเชิงพฤติกรรมมาแล้ว ขอบเขตที่ผู้รีวิวต้องตรวจจึงแคบลงมาก แต่การตัดสินใจเมิร์จยังเป็นของมนุษย์
- หนึ่งลูปใช้เวลานานเท่าไร
- ราวหลักสิบวินาทีต่อรอบต่อข้อบกพร่องหนึ่งรายการ และใช้เฉลี่ย 2.3 รอบจึงปิดผนึก การสแกนทั้งรีโพมีมัธยฐาน 41 วินาที และสามารถตั้งค่าให้ลูปทำงานเฉพาะข้อบกพร่องระดับวิกฤตและสูงที่ยืนยันแล้วได้
อ่านต่อ
บทความทั้งหมดFlawDetector-LLM V1.0: การทดลองซ้ำ 1,000 ครั้ง และสิ่งที่ความแปรปรวนบอกเรา
อัตราตรวจพบ 94.7% ผลบวกลวง 3.2% ความสม่ำเสมอ 99.1% จากคำตัดสิน 1.2 ล้านครั้ง — ความแปรปรวนอยู่ตรงไหน และสามการเปลี่ยนแปลงที่ปิดช่องว่างนั้น
ผลบวกลวงเป็นปัญหาเชิงวัฒนธรรมก่อนจะเป็นปัญหาของโมเดล
ผลบวกลวง 3.2% คือ 138 รายการต่อสัปดาห์บนโค้ด 2 ล้านบรรทัด แยกสี่สถานการณ์ที่เรียกรวมว่าผลบวกลวง แล้วใช้กฎการระงับที่มีวันหมดอายุ