DEV Community

Cover image for วิธีทดสอบ API ของคุณกับอินพุตอันตราย (ก่อนที่แฮกเกอร์จะทำ)
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

วิธีทดสอบ API ของคุณกับอินพุตอันตราย (ก่อนที่แฮกเกอร์จะทำ)

TL;DR: อินพุตของ API คือพื้นที่โจมตี ดังนั้นควรทดสอบเหมือนเป็นพื้นที่โจมตี เขียนกรณีทดสอบเชิงลบที่ส่งฟิลด์ขนาดใหญ่เกินไป ชนิดข้อมูลผิดพลาด เนื้อหาผิดรูปแบบ และสตริงสำหรับฉีดโค้ด จากนั้นยืนยันว่า Endpoint ตอบกลับด้วย 4xx และไม่เคยตอบด้วย 5xx เปลี่ยน Schema validation ให้เป็นการควบคุมความปลอดภัยด้วย additionalProperties: false, enums และการจำกัดความยาว รันชุดทดสอบทั้งหมดใน CI ทุกครั้งที่มีการเปลี่ยนแปลง AI agents ทำให้เรื่องนี้เร่งด่วนขึ้น เพราะพวกมันสร้างและส่ง Payload ด้วยความเร็วของเครื่องจักร ดังนั้น “โหลดข้อมูลนี้” ที่ค่อย ๆ กลายเป็น “รันโค้ดนี้” จึงขยายขนาดได้

ชุดทดสอบส่วนใหญ่มักพิสูจน์เพียงว่า API ทำงานเมื่อผู้เรียกใช้งานส่งข้อมูลอย่างถูกต้อง: ส่ง Payload ที่ถูกต้อง ได้ 200 แล้วจบ แต่ผลลัพธ์นี้แทบไม่ได้บอกว่า API จะตอบสนองอย่างไรเมื่อได้รับข้อมูลอันตราย อินพุตที่ไม่น่าเชื่อถือคือข้อมูลทุกอย่างที่ Endpoint ไม่ได้สร้างเอง เช่น request body, query string, headers, ไฟล์อัปโหลด, webhook payload และ JSON ที่ AI agent สร้างขึ้นแบบทันทีทันใด ทั้งหมดควรถูกตั้งสมมติฐานเดียวกัน: ในที่สุดจะมีคนส่งเวอร์ชันที่แย่ที่สุดของมันเข้ามา

ลองใช้ Apidog วันนี้

บทความนี้เน้นภาคปฏิบัติ: สร้างการทดสอบที่ส่งอินพุตแบบที่ผู้โจมตีส่ง แล้วรันอัตโนมัติทุกครั้งที่มีการเปลี่ยนแปลง หมวดหมู่การทดสอบสอดคล้องกับ OWASP API Security Top 10 ซึ่งควรเปิดค้างไว้ในแท็บ Apidog เป็นหนึ่งในเครื่องมือที่ช่วยออกแบบสัญญาและขับเคลื่อนการทดสอบเหล่านี้ได้ แต่แนวคิดทั้งหมดใช้ได้กับทุกเฟรมเวิร์กที่คุณใช้อยู่แล้ว

อินพุตคือพื้นที่โจมตี ไม่ใช่แค่ฟิลด์ในฟอร์ม

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

ทุกฟิลด์ที่ API ยอมรับคือคำสัญญาที่ผู้เรียกสามารถละเมิดได้ และทุกคำสัญญาที่ถูกละเมิดคือเส้นทางเข้าสู่ตรรกะของคุณ ตัวอย่างเช่น:

  • limit ที่ควรเป็นจำนวนเต็มขนาดเล็ก กลายเป็น 999999999
  • filename ที่ควรเป็นชื่อไฟล์ กลายเป็น ../../etc/passwd
  • config ที่ควรเก็บการตั้งค่า กลายเป็นชุดคำสั่งที่ถูกประเมินค่า

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

เริ่มจากคำถามนี้สำหรับทุกฟิลด์:

สิ่งที่แย่ที่สุดที่อาจถูกส่งเข้ามาในฟิลด์นี้คืออะไร?

เมื่อทำอย่างสม่ำเสมอ คุณกำลังเดินตามหลักของ แนวทางปฏิบัติที่ดีที่สุดด้านความปลอดภัยของ API แล้ว ขั้นต่อไปคือเปลี่ยนคำถามนี้เป็นกรณีทดสอบที่รันได้จริง

“โหลดข้อมูลนี้” กลายเป็น “รันโค้ดนี้” ได้อย่างไร

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

รูปแบบความล้มเหลวมีลักษณะดังนี้:

  1. Endpoint รับข้อมูลที่ดูเหมือนเป็น “ข้อมูล”
  2. ระบบโหลดข้อมูลนั้น
  3. เส้นทางการโหลดไปเรียกใช้โค้ดที่ผู้โจมตีควบคุมได้
  4. “โหลดข้อมูลนี้” กลายเป็น “รันโค้ดนี้”

Template injection ก็เป็นปัญหาเดียวกันในขนาดเล็กกว่า: ค่าคอนฟิกูเรชันที่ควรเป็นข้อความธรรมดาถูกนำไปประเมินค่า ทำให้ข้อความกลายเป็นการดำเนินการ

ดังนั้น Endpoint ใดก็ตามที่รับชื่อ loader, format, template, serialized object หรือ configuration blob อาจกำลังรับ “คำสั่ง” อยู่ แม้ไม่ได้ตั้งใจก็ตาม หากคุณไม่เคยส่งคอนฟิกูเรชันอันตรายไปทดสอบ Endpoint นั้น คุณก็ยังไม่ได้พิสูจน์ว่ามันเป็นข้อมูลธรรมดาจริง

ใช้ Schema validation เป็นการควบคุมความปลอดภัย

การควบคุมที่คุ้มค่าที่สุดคือ schema ที่เข้มงวดตรงขอบของระบบ Schema ไม่ใช่แค่เอกสารประกอบ เมื่อระบบปฏิเสธข้อมูลที่ไม่ตรงกับ Schema มันจะกลายเป็นตัวกรองก่อนที่ business logic จะเห็น request

JSON Schema มีเครื่องมือพื้นฐานที่ช่วยสร้างขอบเขตนี้ได้:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["loader", "name"],
  "properties": {
    "loader": {
      "enum": ["csv", "json", "parquet"]
    },
    "name": {
      "type": "string",
      "maxLength": 128,
      "pattern": "^[\\w .-]+$"
    },
    "rows": {
      "type": "integer",
      "minimum": 0,
      "maximum": 1000000
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Schema นี้มีการป้องกันที่แยกจากกัน 4 ชั้น:

การควบคุม สิ่งที่ช่วยป้องกัน
additionalProperties: false ปฏิเสธฟิลด์ที่ไม่ได้อนุญาต เช่น template ที่ถูกลักลอบเพิ่มมา
enum สำหรับ loader ป้องกันค่าอย่าง pickle:// หรือตัวโหลดโค้ดระยะไกลที่ไม่ได้อยู่ใน allowlist
maxLength ลดความเสี่ยงจากสตริงขนาดหลายเมกะไบต์ที่ใช้หน่วยความจำสูง
pattern ปฏิเสธอักขระหรือรูปแบบที่ไม่ควรอยู่ในชื่อ เช่น {{ หรือ '; DROP TABLE

แนวคิดสำคัญคือ Schema ไม่จำเป็นต้องรู้จักผู้โจมตี มันเพียงอนุญาตชุดอินพุตแคบ ๆ ที่ระบบรองรับจริงเท่านั้น และความแคบนั้นคือคุณสมบัติด้านความปลอดภัย

อย่างไรก็ตาม Schema ไม่สามารถหยุดการโจมตีได้ทุกชนิด มันช่วยปิดช่องโหว่คลาสสำคัญคือ:

เราไม่เคยตรวจสอบว่า Endpoint นี้ยอมรับอะไรบ้าง

Negative testing: พิสูจน์ว่า Endpoint ปฏิเสธ

Happy-path test ยืนยันว่าอินพุตที่ดีสร้างเอาต์พุตที่ดี

Negative test ยืนยันว่าอินพุตที่ไม่ดีถูกปฏิเสธอย่างควบคุมได้

ความต่างนี้สำคัญมาก:

  • 400 หรือ 422 พร้อมข้อผิดพลาดที่เหมาะสม หมายถึง API ปกป้องขอบเขตของตัวเอง
  • 500 หมายถึง Payload ไปถึงโค้ดที่ไม่พร้อมรับมัน

สำหรับแต่ละฟิลด์ ให้สร้างกรณีเชิงลบอย่างน้อยตามรายการนี้:

  • ชนิดข้อมูลผิด
  • ไม่มีฟิลด์ที่บังคับ
  • มีฟิลด์ที่ห้ามมี
  • ค่ายาวเกินไป
  • ค่านอกช่วง
  • สตริงสำหรับการฉีดโค้ดที่สอดคล้องกับบริบทของฟิลด์

จากนั้นยืนยันอย่างน้อย 2 เงื่อนไข:

1. สถานะต้องเป็น 4xx
2. สถานะต้องไม่เป็น 5xx
Enter fullscreen mode Exit fullscreen mode

รายการตรวจสอบการทดสอบความปลอดภัยของ API ช่วยใช้เป็นจุดเริ่มต้นสำหรับการไล่ตรวจแบบฟิลด์ต่อฟิลด์ได้

อย่ายืนยันข้อความ error แบบตายตัว เช่น "invalid loader" เพราะการปรับปรุงข้อความโดยไม่กระทบความปลอดภัยอาจทำให้ชุดทดสอบล้มเหลวโดยไม่จำเป็น ให้ยืนยันพฤติกรรมแทน:

  • status code
  • ไม่มี 5xx
  • ไม่มีผลข้างเคียง เช่น ไม่มีข้อมูลถูกสร้างหรือแก้ไข

คลาสการฉีดโค้ดที่ควรมีกรณีทดสอบถาวร

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

เครื่องมือสำหรับ การตรวจจับช่องโหว่ API อัตโนมัติ ช่วยขยายความครอบคลุมได้ภายหลัง แต่กรณีที่เขียนเองไม่กี่กรณีก็จับปัญหาที่ชัดเจนได้มาก

SQL injection

ส่ง Payload เช่นนี้ไปยังฟิลด์ที่อาจถูกนำไปสร้าง query:

1); DROP TABLE datasets;--
Enter fullscreen mode Exit fullscreen mode

Endpoint ควรถือค่าเป็น literal value และควร:

  • ตอบกลับ 400 หรือผลลัพธ์ว่าง
  • ไม่แสดงข้อผิดพลาดจากฐานข้อมูล
  • ไม่เปลี่ยนแปลงข้อมูลในฐานข้อมูล

Template injection

ส่งค่าเหล่านี้ในฟิลด์ชื่อหรือป้ายกำกับ:

{{ 7*7 }}
{{ config.__class__ }}
Enter fullscreen mode Exit fullscreen mode

หาก response มี 49 อยู่ แปลว่า template engine ประเมินอินพุตแล้ว นั่นคือสัญญาณของ server-side template injection

Insecure deserialization และ remote-code loader

ส่ง loader ที่ไม่ได้รับอนุญาต:

{
  "loader": "pickle://s3/models/payload.pkl"
}
Enter fullscreen mode Exit fullscreen mode

หรือส่ง serialized object ในตำแหน่งที่ควรรับค่าธรรมดา Endpoint ควรปฏิเสธ loader ที่ไม่อยู่ใน allowlist ทันที ไม่ควรพยายามเดาหรือประมวลผลให้

Command injection

ส่งค่าเหล่านี้ไปยังฟิลด์ที่อาจกลายเป็น shell argument เช่น filename หรือ conversion option:

; id
$(id)
Enter fullscreen mode Exit fullscreen mode

หาก response 200 และเปิดเผยข้อมูลอย่าง user ID นั่นคือช่องโหว่ที่ต้องแก้ทันที ไม่ใช่ผลการทดสอบที่น่าสนใจ

ข้อมูลขนาดใหญ่เกินไป ข้อมูลผิดรูปแบบ และ Content-Type confusion

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

ทดสอบ Payload ขนาดใหญ่ เช่น:

  • ฟิลด์เดียวที่มีตัวอักษร 5 MB
  • JSON array ที่มี 1 ล้านองค์ประกอบ
  • JSON ที่ซ้อนกันลึกหลายพันระดับ

API ที่ดีควร:

  • จำกัดขนาด request body
  • ตอบกลับ 413 Payload Too Large สำหรับ body ที่เกินขีดจำกัด
  • ตอบกลับ 400 อย่างรวดเร็วสำหรับ JSON ที่ผิดรูปแบบ
  • ไม่ใช้หน่วยความจำจน worker ล่มหรือค้าง

ตัวอย่าง JSON ที่ควรทดสอบ:

{
  "loader": "csv",
  "name": "AAAAAAAAAAAAAAAAAAAAAAAA..."
}
Enter fullscreen mode Exit fullscreen mode

และทดสอบ JSON ที่ผิดรูปแบบ เช่น:

{
  "loader": "csv",
  "name": "dataset",
}
Enter fullscreen mode Exit fullscreen mode

รวมถึง Content-Type confusion:

Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

แต่ส่ง XML เป็น body หรือกลับกัน:

Content-Type: application/xml
Enter fullscreen mode Exit fullscreen mode

แต่ส่ง JSON เป็น body

ลองส่ง JSON ด้วย text/plain ด้วย เพื่อดูว่า parser ยอมรับข้อมูลที่ไม่ตรงกับ Content-Type หรือไม่ เซิร์ฟเวอร์ควรตรวจสอบให้ header และ body สอดคล้องกันก่อน parse ข้อมูล

เหตุใด AI agents จึงเพิ่มความเสี่ยง

ความเสี่ยงทั้งหมดข้างต้นมีอยู่ก่อน AI agents แต่ agent เปลี่ยน “ปริมาณ” และ “ความเร็ว”

ผู้โจมตีที่เป็นมนุษย์ส่ง request อันตรายทีละรายการ แต่ AI agent สามารถสร้างและส่ง Payload ด้วยความเร็วของเครื่องจักร รวมถึงสร้างค่าฟิลด์ที่มนุษย์ไม่เคยคิดจะทดสอบ

สิ่งที่ทำให้ agent เพิ่มความเสี่ยงมี 3 ข้อ:

  1. สร้างอินพุตใหม่ได้เอง

    Agent สังเคราะห์ค่าที่ไม่มีใครเขียนไว้ล่วงหน้า และอาจไม่อยู่ใน test case เดิม

  2. พยายามซ้ำได้รวดเร็ว

    เอกสารต้นทางอันตรายหนึ่งฉบับอาจกลายเป็น request จำนวนมากต่อ Endpoint ภายในไม่กี่วินาที

  3. ส่งต่อข้อมูลข้ามขอบเขตความเชื่อถือ

    Payload ที่ซ่อนใน dataset หรือ webhook สามารถถูก agent นำไปส่งต่อยัง API จริงได้

รูปแบบ “โหลดข้อมูลนี้” กลายเป็น “รันโค้ดนี้” เป็นประเภทคำสั่งที่ agent อาจดำเนินการข้าม trust boundary โดยไม่สังเกตเห็น อ่านเพิ่มเติมได้ในบทความ การฉีด Prompt สำหรับทีม API

การป้องกันยังเหมือนเดิม แต่ต้องเป็นแบบอัตโนมัติ เพราะไม่สามารถตรวจ request จาก agent ด้วยมือได้ทัน

สร้างชุดทดสอบเชิงลบและรันใน CI ทุกครั้งที่มีการเปลี่ยนแปลง

เปลี่ยนกรณีด้านบนให้เป็นชุดทดสอบที่รันในทุก Pull Request ตัวอย่างนี้ใช้ pytest และ httpx เรียก Endpoint บน staging:

import httpx
import pytest

BASE = "https://staging.internal/v1"

HOSTILE_CONFIGS = [
    {"loader": "pickle://s3/models/payload.pkl", "format": "auto"},  # ตัวโหลดโค้ดระยะไกล
    {"loader": "csv", "name": "{{ 7*7 }}"},                          # การฉีดเทมเพลต
    {"loader": "csv", "name": "{{ config.__class__ }}"},             # การสำรวจวัตถุ
    {"loader": "csv", "filter": "1); DROP TABLE datasets;--"},       # SQL injection
    {"loader": "csv", "name": "A" * 5_000_000},                      # ฟิลด์ขนาดใหญ่เกินไป
]

@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
    r = httpx.post(
        f"{BASE}/datasets",
        json={"config": config},
        timeout=10,
    )

    assert r.status_code in (400, 413, 422), r.text
    assert r.status_code < 500, (
        "5xx หมายความว่า Payload ไปถึงตรรกะที่ไม่ควรเข้าถึง"
    )
    assert "49" not in r.text, (
        "เทมเพลตถูกเรนเดอร์: อาจมี server-side template injection"
    )
Enter fullscreen mode Exit fullscreen mode

จุดสำคัญของ test นี้ไม่ใช่การตรวจข้อความ error แต่เป็นการยืนยันว่า:

  • Endpoint ปฏิเสธ Payload
  • การตอบกลับเป็น 4xx
  • ไม่มี 5xx
  • template ไม่ถูก render

จากนั้นเชื่อมเข้ากับ CI เพื่อให้ test เป็นเงื่อนไขก่อน merge ตัวอย่าง GitHub Actions ขั้นต่ำ:

name: api-abuse-tests

on: [push, pull_request]

jobs:
  negative-input:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/negative_input.py -q
Enter fullscreen mode Exit fullscreen mode

สำหรับ workflow ที่เน้น Schema คุณสามารถใช้ Apidog ออกแบบ Endpoint ตามสัญญา OpenAPI และบันทึก scenario เชิงลบไว้ข้าง happy-path scenario ได้ เช่น:

  • ฟิลด์ขนาดใหญ่เกินไป
  • ชนิดข้อมูลผิด
  • ฟิลด์ที่ไม่อนุญาต
  • SQL injection
  • template injection
  • loader ที่ไม่อยู่ใน allowlist

แต่ละ scenario ควรยืนยันว่า response เป็น 4xx แล้วรัน scenario เดิมใน CI ผ่าน Apidog CLI เพื่อให้การเปลี่ยนแปลงที่ลดความเข้มงวดของ validation ทำให้ build ล้มเหลวก่อน deploy

หากต้องการเริ่มใช้งาน ให้ ดาวน์โหลด Apidog และเพิ่ม negative scenario หนึ่งกรณีให้ Endpoint ที่มีอยู่ก่อน

ควรเข้าใจขอบเขตของเครื่องมือให้ชัดเจน: Apidog เป็นเครื่องมือสำหรับออกแบบ ทดสอบ mock และจัดทำเอกสาร API ไม่ได้ทำหน้าที่เป็น WAF, ไม่กรองทราฟฟิกแบบเรียลไทม์ และไม่แทนที่ SIEM หรือระบบ monitoring สิ่งที่มันช่วยได้คือทำให้สัญญาของ API ชัดเจน และทำให้ทีมตรวจสอบอย่างต่อเนื่องว่า Endpoint ยอมรับอะไรบ้าง

คำถามที่พบบ่อย

Negative testing ต่างจาก fuzzing อย่างไร?

Negative testing ส่งชุดอินพุตไม่ดีที่เลือกมาอย่างตั้งใจ โดยเลือกหนึ่งหรือไม่กี่กรณีต่อ failure mode ที่สนใจ ส่วน fuzzing สร้างหรือดัดแปลงอินพุตจำนวนมากเพื่อค้นหากรณีที่ทีมอาจนึกไม่ถึง

เริ่มจาก negative testing ก่อน เพราะรวดเร็ว กำหนดผลลัพธ์ได้ และรันใน CI ได้ง่าย จากนั้นเพิ่ม fuzzing เมื่อต้องการความครอบคลุมที่กว้างขึ้น

ควรรันการทดสอบเหล่านี้กับ Production หรือไม่?

ไม่ควร ควรรันกับ staging หรือ test environment ที่แยกออกมาโดยเฉพาะ Payload บางชนิด เช่น request ขนาดใหญ่หรือ command injection ถูกออกแบบมาเพื่อกดดันระบบ และอาจเปลี่ยนแปลงข้อมูลได้หากมีบั๊ก

WAF จับสิ่งเหล่านี้ได้อยู่แล้วไม่ใช่หรือ?

WAF เป็นการป้องกันเชิงลึกที่มีประโยชน์ แต่ไม่ใช่สิ่งทดแทน validation ในแอปพลิเคชัน กฎ WAF อาจถูก bypass ได้ และไม่เข้าใจ business logic ของ API เป้าหมายของ test เหล่านี้คือพิสูจน์ว่า Endpoint ปฏิเสธข้อมูลอันตรายได้ด้วยตัวเอง

ควรมี negative test กี่กรณีต่อ Endpoint?

ตั้งเป้าอย่างน้อยหนึ่งกรณีต่อฟิลด์ต่อ failure class ที่เป็นไปได้:

  • ชนิดข้อมูลผิด
  • ค่านอกช่วง
  • ค่ายาวเกินไป
  • ฟิลด์ต้องห้าม
  • สตริงฉีดโค้ดที่ตรงกับบริบท

โดยทั่วไปจึงมีเพียงไม่กี่กรณีต่อ Endpoint ไม่จำเป็นต้องมีหลายร้อยกรณี ความครอบคลุมของคลาสความผิดพลาดสำคัญกว่าจำนวน test ดิบ

Schema validation หยุด code injection ได้ทั้งหมดหรือไม่?

ไม่ได้ และไม่ควรเป็นชั้นป้องกันเดียว Schema ที่เข้มงวดช่วยกำจัดข้อมูลผิดรูปแบบ ขนาดใหญ่เกินไป และฟิลด์ที่ไม่คาดคิด แต่ค่าที่ถูกต้องตาม Schema ก็ยังอาจเป็น SQL injection หรือ template injection ได้

จึงควรใช้ร่วมกับ:

  • parameterized queries
  • safe deserialization
  • output encoding
  • allowlists
  • schema validation

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

Top comments (0)