วิศวกรรมอ่าน 9 นาที

ลูปแพตช์เชิงปฏิปักษ์ตัดสินอย่างไรว่าช่องโหว่ถูกปิดผนึกจริง

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

โดย ทีมวิศวกรรมความปลอดภัย FloatFactory

เอนจิน FlawDetector · งานวิจัย

การสแกนซ้ำไม่ใช่ข้อพิสูจน์

เวิร์กโฟลว์มาตรฐานของการวิเคราะห์แบบสถิตมักจบแบบนี้: พบข้อบกพร่อง เสนอการแก้ไข นักพัฒนาเมิร์จแพตช์ สแกนรอบถัดไปยืนยันว่าข้อบกพร่องหายไป แล้วปิดทิกเก็ต ปัญหาคือขั้นตอนสุดท้ายไม่ได้พิสูจน์อะไรเลย การตัดสินที่สร้างข้อบกพร่องกับการตัดสินที่ประกาศว่ามันหายไป มาจากเอนจินเดียวกัน กฎชุดเดียวกัน และมุมมองเดียวกัน เท่ากับคนออกข้อสอบตรวจคำตอบของตัวเอง

ในรีโพซิทอรีของลูกค้า เราพบรูปแบบความล้มเหลวซ้ำ ๆ สามแบบ แบบแรกคือ การเลี่ยงลายเซ็น แพตช์เปลี่ยนเฉพาะรูปแบบสตริงที่กฎจับได้ สแกนเนอร์จึงเงียบลงทั้งที่เส้นทางโจมตียังเปิดอยู่ แบบที่สองคือ การบรรเทาบางส่วน ปิดเฉพาะทางเข้าที่เห็นชัดที่สุด แล้วปล่อยแฮนด์เลอร์ตัวที่สองซึ่งมีจุดอ่อนเดียวกันไว้ตามเดิม แบบที่สามคือ การย้ายตำแหน่ง ตรรกะตรวจสอบถูกยกขึ้นไปชั้นบน แล้วเปิดช่องอ้อมใหม่ระหว่างทาง ทั้งสามกรณีนี้ผลสแกนซ้ำออกมาสะอาดหมด

สแกนเนอร์เงียบลงกับการโจมตีล้มเหลวเป็นคนละเหตุการณ์กัน ไปป์ไลน์ส่วนใหญ่วัดอย่างแรกแล้วรายงานอย่างหลัง

ห้าเฟสของหนึ่งรอบ

ลูปแพตช์เชิงปฏิปักษ์ให้เอเจนต์สองตัวปะทะกันภายใน CI โดย AI เรดทีมรับข้อบกพร่องที่ยืนยันแล้วหนึ่งรายการแล้วพยายามเจาะจริง ส่วน AI บลูทีมเขียนแพตช์ที่เล็กที่สุดซึ่งทำให้ความพยายามนั้นล้มเหลว หนึ่งรอบประกอบด้วยห้าเฟส และแต่ละเฟสทิ้งผลผลิตไว้ให้เฟสถัดไปใช้

  1. 01

    โจมตี — เรดทีมวางแผนเอ็กซ์พลอยต์

    รับตำแหน่งข้อบกพร่อง กราฟการเรียก และรายการจุดเข้า แล้วประกอบลำดับการโจมตีหลายขั้น ผลผลิตคือลำดับคำขอที่รันได้จริง พร้อมเงื่อนไขสังเกตที่ใช้ตัดสินความสำเร็จ ได้แก่ เนื้อหาการตอบกลับ สถานะโค้ด และผลข้างเคียง

  2. 02

    ตรวจจับการเจาะ — รันในสภาพแวดล้อมแยก

    สร้างรีโพจากคอมมิตที่ทดสอบลงคอนเทนเนอร์ใช้แล้วทิ้งแล้วรันลำดับนั้น เมื่อเงื่อนไขสังเกตถูกเติมเต็ม ข้อบกพร่องจะเลื่อนสถานะจาก “ความเสี่ยงเชิงทฤษฎี” เป็น “เอ็กซ์พลอยต์ที่ทำซ้ำได้” พร้อมบันทึกร่องรอยไว้

  3. 03

    สร้างแพตช์ — บลูทีมเปลี่ยนให้น้อยที่สุด

    อินพุตของบลูทีมคือร่องรอยเอ็กซ์พลอยต์ ไม่ใช่โค้ดที่บกพร่อง เป้าหมายคือการเปลี่ยนแปลงที่เล็กที่สุดซึ่งทำให้ร่องรอยนี้ล้มเหลว ส่วนการรีแฟกเตอร์และการปรับสไตล์ถูกกันออกอย่างชัดเจน

  4. 04

    โจมตีซ้ำ — ยิงทั้งของเดิมและของกลายพันธุ์

    รีเพลย์ลำดับต้นฉบับกับทรีที่แพตช์แล้ว จากนั้นยิงชุดกลายพันธุ์ตาม หากยังมีอันใดทะลุ ผลนั้นจะกลายเป็นอินพุตของบลูทีมในรอบถัดไป

  5. 05

    ยืนยันการปิดผนึก — ตรวจว่าไม่มีอะไรพัง

    แพตช์ที่ผ่านด่านความปลอดภัยยังต้องผ่านชุดทดสอบเดิมและการเทียบการตอบกลับกับอินพุตปกติ แพตช์ที่หยุดการโจมตีด้วยการทำให้ฟีเจอร์พังไม่นับว่าปิดผนึก

ด่านสามชั้นของลูปแพตช์เชิงปฏิปักษ์

คำว่า “ปิดผนึก” ที่นี่ไม่ใช่ถ้อยคำการตลาด แต่เป็นป้ายที่ติดได้ก็ต่อเมื่อเงื่อนไขบูลีนสามข้อเป็นจริงพร้อมกัน หากข้อใดเป็นเท็จ สถานะจะไม่ใช่ปิดผนึก และบันทึกการตัดสินจะระบุว่าติดที่ด่านใดเพราะอะไร

ด่านยืนยันการปิดผนึก
ด่านเงื่อนไขผ่านเมื่อไม่ผ่าน
เอ็กซ์พลอยต์ต้นฉบับลำดับที่สำเร็จในรอบแรกถูกรีเพลย์สามครั้งและล้มเหลวทุกครั้งแพตช์ไม่ได้แตะเส้นทางโจมตี ส่งร่องรอยกลับให้บลูทีมเขียนใหม่
ชุดกลายพันธุ์การกลายพันธุ์ 12 รายการจากสามตระกูล — การเข้ารหัส การย้ายบริบท และการเรียงลูกโซ่ใหม่ — ล้มเหลวทั้งหมดเป็นแพตช์ที่กันเฉพาะลายเซ็น นำการกลายพันธุ์ที่ทะลุใส่บริบทเข้ารอบถัดไป
การถดถอยและความเท่าเทียมเชิงพฤติกรรมชุดทดสอบเดิมผ่านทั้งหมด และการตอบกลับต่ออินพุตปกติเหมือนก่อนแพตช์ปลอดภัยแต่ฟีเจอร์พัง ต้องลดขอบเขตการเปลี่ยนแปลงแล้วลองใหม่

ทั้งสามด่านถูกประเมินตามลำดับ หากด่านก่อนหน้าไม่ผ่าน ด่านถัดไปจะไม่ถูกรัน

การกลายพันธุ์ถูกสร้างขึ้นอย่างไร

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

  • ตระกูลการเข้ารหัส — URL encoding ซ้อนสองชั้น ความต่างของ Unicode normalization การแทรก null byte และการสลับตัวพิมพ์ ทั้งหมดคือความหมายเดิมที่แสดงด้วยไบต์ต่างออกไป ตัวกรองที่อิงเรกูลาร์เอ็กซ์เพรสชันมักพังตรงนี้
  • ตระกูลการย้ายบริบท — เพย์โหลดเดิมถูกส่งผ่านจุดเข้าอื่น เช่น เราต์อื่น batch API ตัวรับ webhook หรือแฮนด์เลอร์ที่เปิดเฉพาะผู้ดูแลระบบ นี่คือด่านที่จับการบรรเทาบางส่วน
  • ตระกูลการเรียงลูกโซ่ใหม่ — สลับลำดับของการโจมตีหลายขั้น หรือแทนขั้นกลางด้วยวิธีอื่น เช่น เปลี่ยนลูกโซ่ auth bypass → อ่านไฟล์ ให้ยาวขึ้นเป็น auth bypass → path traversal → อ่านไฟล์

ค่าเริ่มต้นคือ 12 การกลายพันธุ์ โดยถ่วงน้ำหนักตามชนิดข้อบกพร่อง กลุ่ม injection จะหนักไปทางตระกูลการเข้ารหัส ส่วนข้อบกพร่องด้านสิทธิ์มีการย้ายบริบทเกินครึ่ง การสร้างเป็นแบบกำหนดผลได้ภายใต้ seed ที่ตรึงไว้ ข้อบกพร่องเดียวกันจึงสร้างชุดเดิมได้เสมอ เพราะในการตรวจสอบภายหลังต้องแสดงให้เห็นได้ว่าตอนนั้นยิงอะไรไปบ้าง

เมื่อปิดผนึกไม่ได้ — บรรเทาและยกระดับ

ลูปไม่ได้วนไม่รู้จบ เพดานเริ่มต้นคือสี่รอบ และในแต่ละรอบบลูทีมจะรับบริบทเต็มของความล้มเหลวครั้งก่อนไปด้วย เมื่อถึงเพดาน ข้อบกพร่องจะลงเอยที่หนึ่งในสามสถานะปลายทาง

  1. sealed (ปิดผนึก) — ผ่านครบทั้งสามด่าน diff ของแพตช์และบันทึกการตัดสินจะถูกเปิดเป็น pull request
  2. mitigated (บรรเทาแล้ว) — การโจมตีต้นฉบับล้มเหลว แต่การกลายพันธุ์บางรายการยังทะลุ ขั้นตอนการทำซ้ำของรายการที่รอดจะถูกแนบไว้ครบ
  3. escalated (ยกระดับ) — ข้อบกพร่องเชิงโครงสร้างที่แพตช์เล็กที่สุดแก้ไม่ได้ ถูกส่งต่อให้มนุษย์พร้อมข้อสรุปว่าต้องเปลี่ยนการออกแบบ ตัวอย่างคลาสสิกคือโมเดลการให้สิทธิ์ที่ผิดตั้งแต่ต้น

2.3

จำนวนรอบเฉลี่ยจนปิดผนึก

ข้อบกพร่องระดับวิกฤตและสูง มัธยฐาน 2 รอบ

88.4%

ปิดผนึกได้ภายในสี่รอบ

จากข้อบกพร่องที่เข้าลูป

8.1%

ปิดในสถานะบรรเทา

ความเสี่ยงลดลงแต่ยังมีการกลายพันธุ์รอด

3.5%

ยกระดับให้มนุษย์

ส่วนใหญ่เป็นเรื่องการออกแบบและโมเดลสิทธิ์

วิธีอ่านบันทึกการตัดสิน

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

verdict.jsonjson
{
  "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"
}
ข้อบกพร่อง SQL injection ที่ถูกปิดผนึกภายในสองรอบ

ฟิลด์ที่สำคัญที่สุดไม่ใช่ verdict แต่เป็น gates เพราะ verdict บอกแค่ผ่านหรือไม่ผ่าน ส่วน gates เผยให้เห็นว่าการตัดสินนั้นอิงอะไร และเนื่องจากมีการบันทึก mutation_suite.seed ไว้ ผู้ตรวจสอบในอีกหกเดือนข้างหน้าจึงสร้างชุดกลายพันธุ์เดิมขึ้นมาใหม่และยืนยันผลเดียวกันได้

ต่อเข้ากับ CI ในฐานะเกต

คุณค่าที่แท้จริงของลูปปรากฏเมื่อใช้เป็นเกตการเมิร์จ การตั้งค่าที่พบบ่อยคือกำหนดสถานะปลายทางต่างกันตามระดับความรุนแรง ระดับวิกฤตและสูงต้องถึงสถานะ sealed ส่วนระดับกลางลงมายอมรับ mitigated ได้

.github/workflows/security-gate.ymlyaml
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 วินาที และสามารถตั้งค่าให้ลูปทำงานเฉพาะข้อบกพร่องระดับวิกฤตและสูงที่ยืนยันแล้วได้

ดูลูปทำงานจริงบนรีโพของคุณ

เชื่อมต่อรีโพแล้วรับการสแกนเต็มรูปแบบ แพตช์ที่พร้อมเมิร์จ และบันทึกการตัดสินของทุกข้อบกพร่องระดับวิกฤต ไม่ต้องผูกบัตร