บทความนี้แปลจากต้นฉบับภาษาอังกฤษ อ่านต้นฉบับ
วิศวกรรมความปลอดภัยสำหรับคอนเน็กเตอร์ของเอเจนต์ AI: ความล้มเหลวเก้าประเภท
StaffOS ป้องกันเครื่องมือภายนอกที่เอเจนต์ AI เรียกใช้อย่างไร: ความล้มเหลวเก้าประเภท ตั้งแต่ SSRF และเทมเพลต URL ถึงขอบเขตของ secret และการควบคุมของแต่ละประเภท
เอเจนต์ AI ที่เรียก API ภายนอกอ่านข้อมูลส่วนตัวได้ อ่านเนื้อหาที่ไม่มีใครตรวจสอบ และส่งข้อมูลออกไปได้ ซอฟต์แวร์ที่ทำงานรอบโมเดลเป็นผู้ตัดสินว่าเครื่องมือเข้าถึงโฮสต์ใดได้ คำขอแต่ละครั้งนำอะไรไปด้วย ใครมีสิทธิ์ชี้ secret ที่บันทึกไว้ไปยังปลายทางใด และอะไรจะกลับมาถึงโมเดล บทความนี้อธิบายวิธีที่ StaffOS ตัดสินเรื่องเหล่านี้สำหรับเครื่องมือภายนอกที่เอเจนต์ของเราเรียกใช้ ได้แก่ ความล้มเหลวเก้าประเภท การควบคุมที่เราสร้างสำหรับแต่ละประเภท และรายละเอียดในซอฟต์แวร์ HTTP ที่ใช้กันทั่วไปซึ่งเป็นตัวตัดสินว่าการควบคุมนั้นมีผลหรือไม่
hostname, secret และระเบียนในตัวอย่างเป็นข้อมูลสมมติ ข้อความที่กล่าวถึงซอฟต์แวร์ของบุคคลที่สามอ้างอิงเอกสารหรือซอร์สโค้ดของซอฟต์แวร์นั้น
รายการตรวจสอบ
การตรวจสิบสองข้อนี้ใช้กับคอนเน็กเตอร์ทุกตัวที่ส่งคำขอไปยัง URL ที่ผู้ใช้ของคอนเน็กเตอร์ตั้งค่าเอง แต่ละข้อระบุอินพุตที่ใช้ทดสอบ และส่วนที่อธิบายเรื่องนั้น
- ตัดสิน URL ที่ transport จะส่ง
http://{x}127.0.0.1/adminต้องถูกปฏิเสธ และต้องไม่ถูก HTTP client expand ไปเป็น loopback (ส่วนที่ 3) - ปฏิเสธทุกแอดเดรสที่ไม่ globally reachable โดยใช้ตารางที่ระบุชัดเจน
100.100.100.200,64:ff9b::7f00:1และ::ffff:127.0.0.1ต้องถูกปฏิเสธทั้งหมด (ส่วนที่ 2) - Pin แอดเดรสที่ตรวจแล้ว โดยใช้โฮสต์ตรงตามที่เขียนทุกตัวอักษรเป็นคีย์ คำขอไปยัง
api.example.test.ต้องเชื่อมต่อไปยังแอดเดรสที่ pin ไว้ และต้องไม่ resolve ใหม่ (ส่วนที่ 2) - ปิด redirect และปิดการตั้งค่า proxy จาก environment เมื่อตั้ง
HTTPS_PROXYไว้ คำขอต้องยังเชื่อมต่อโดยตรง และ 302 ต้องกลับมาถึงคุณโดยไม่มีการตาม redirect (ส่วนที่ 2) - กำหนด deadline ให้ DNS ที่คุณบังคับใช้ได้ name server ที่ไม่ตอบเลยต้องไม่กักเวิร์กเกอร์ไว้เกิน timeout ของเครื่องมือ (ส่วนที่ 2)
- เขียนแต่ละค่าตามตำแหน่งที่ค่านั้นอยู่
Tan Ah Kowต้องไปถึง JSON body เป็นTan Ah Kowเครื่องหมายอัญประกาศในค่าต้องไม่เพิ่มคีย์ และอาร์กิวเมนต์ใน path ที่เป็น..ต้องถูกปฏิเสธ (ส่วนที่ 3) - กำหนดขอบเขตของ secret ที่บันทึกไว้ทั้งตามการใช้งานและตามเจ้าของ สมาชิกที่ไม่มีสิทธิ์แอดมินต้องไม่สามารถเปลี่ยน URL ของเครื่องมือที่ส่ง secret ที่บันทึกไว้ หรือรันเครื่องมือนั้นได้ (ส่วนที่ 4)
- ให้การเรียกอัตโนมัติมีข้อกำหนดของตัวเอง เครื่องมือ POST ที่ไม่มีเจ้าของคนใดรับรองว่าเป็นแบบอ่านอย่างเดียว ต้องไม่มีวันถูกเรียกแบบอัตโนมัติ (ส่วนที่ 7)
- เก็บค่าของแพลตฟอร์มให้พ้นมือโมเดล สคีมาที่ประกาศอาร์กิวเมนต์
phone_numberควบคู่กับ{{conversation:phone_number}}ต้องถูกปฏิเสธ (ส่วนที่ 8) - แทนที่ข้อความข้อผิดพลาดของ transport และปกปิดค่าที่ทราบอยู่แล้วในทุกรูปแบบการเขียน ความล้มเหลวของ DNS ต้องคืนประโยคตายตัวที่ไม่ระบุชื่อโฮสต์ใด และ
{"echo":"sk\u002dlive\u002d4f9c"}ต้องกลับมาในสภาพที่ถูกปกปิดแล้ว (ส่วนที่ 5) - ถือว่าสิ่งที่อ่านไม่ได้ไม่ปลอดภัย โหมดการยืนยันตัวตนที่ไม่รู้จัก ข้อมูลรับรองที่แพลตฟอร์มถอดรหัสไม่ได้ และ JSON body ที่ซ้อนลึกเกินขีดจำกัดของ decoder แต่ละกรณีต้องนำไปสู่การปฏิเสธหรือการระงับไว้ (ส่วนที่ 4 และ 5)
- จำกัดจำนวนไบต์ขณะที่ข้อมูลกำลังเข้ามา upstream ที่ stream ข้อมูล 100 MB ต้องถูกตัดที่เพดาน โดยไม่มีข้อมูลส่วนที่เกินเพดานถูกบัฟเฟอร์ไว้ (ส่วนที่ 9)
ตัวอย่างสาธิตสี่กรณี
ตัวอย่างแต่ละกรณีด้านล่างเจาะผ่าน guard ที่ดูเหมือนถูกต้อง เราทำซ้ำทั้งสี่กรณีบนเครื่องของเราด้วย curl 8.7.1 และ PHP 8.5.7
1. ตรวจว่า HTTP client ของคุณ expand เทมเพลต URL หรือไม่
URL as configured: http://{x}127.0.0.1/admin
Host a parser sees: {x}127.0.0.1 (not an IP address, no DNS answer)
URL Laravel sends: http://127.0.0.1/admin
guard ที่ถือว่าโฮสต์ที่ไม่มีคำตอบ DNS ไม่เป็นอันตรายจะอนุมัติ URL นี้ จากนั้น HTTP client ของ Laravel ส่ง URL นี้ผ่าน expander ตาม RFC 6570 ของ Guzzle {x} ไม่มีค่าจึง expand เป็นค่าว่าง และคำขอก็ไปถึง loopback ส่วนที่ 3 อธิบายการ expand นี้ และวิธีทำให้ expander ไม่เหลืออะไรให้ทำ
2. ใช้โฮสต์ตรงตามที่คำขอเขียนทุกตัวอักษรเป็นคีย์ของ DNS pin
# The pin applies: cURL connects to 203.0.113.10 without a DNS lookup.
curl --resolve api.example.test:443:203.0.113.10 https://api.example.test/
# The fully qualified name is a different key, so cURL ignores the pin and resolves again.
curl --resolve api.example.test:443:203.0.113.10 https://api.example.test./
guard ที่ตัดจุดท้ายชื่อออกก่อนสร้างคีย์ของ pin ได้ตรวจคำตอบ DNS คำตอบหนึ่ง แต่ปล่อยการเชื่อมต่อไว้กับอีกคำตอบหนึ่ง ส่วนที่ 2 อธิบายเรื่องการ pin
3. Decode ก่อนปกปิด
{ "echo": "sk\u002dlive\u002d4f9c" }
การค้นหาระดับไบต์สำหรับคีย์ที่ทราบอยู่แล้ว sk-live-4f9c ไม่พบอะไรใน body นี้ ขณะที่ JSON decoder คืนคีย์นั้นออกมาตรงทุกตัวอักษร ส่วนที่ 5 แสดงรูปแบบการเขียนอื่นที่ค่าที่ทราบอยู่แล้วอาจอยู่ในรูปนั้นได้
4. ปฏิเสธค่าใน path ที่ไต่ขึ้นไปตาม path
Template as configured: https://api.example.test/v1/customers/{id}/orders
Argument from the model: id = ..
URL after encoding: https://api.example.test/v1/customers/../orders
Path cURL sends: /v1/orders
rawurlencode('..') คืนค่า .. และ libcurl ลบ dot segment ก่อนส่งคำขอ ตามที่ RFC 3986 §5.2.4 อธิบายและเอกสารของ CURLOPT_PATH_AS_IS ระบุไว้ โมเดลที่ถูกชักนำด้วยข้อความของลูกค้าอาจย้ายการเรียกขึ้นไปใน API ของ operator โดยพาข้อมูลรับรองของเครื่องมือเองไปด้วย ส่วนที่ 3 อธิบายการปฏิเสธนี้
1. แบบจำลองภัยคุกคามสำหรับคอนเน็กเตอร์ของเอเจนต์
คอนเน็กเตอร์เปิดทางให้เอเจนต์ AI ดำเนินการนอกแพลตฟอร์ม บน StaffOS เวิร์กสเปซหนึ่งให้เครื่องมือภายนอกแก่เอเจนต์ของตนได้สองประเภท คือ เครื่องมือ HTTP API ที่เวิร์กสเปซกำหนดเอง และ endpoint ระยะไกลที่บริการอื่นโฮสต์ไว้ ทั้งสองประเภททำงานภายในบทสนทนาจริงกับลูกค้า แบบจำลองภัยคุกคามจึงครอบคลุมลูกค้า โมเดล บริการ upstream และทุกคนที่ตั้งค่าเครื่องมือ
เครื่องมือ HTTP API อธิบายคำขอหนึ่งรายการ ได้แก่ เมธอด, เทมเพลต URL, header, โหมดการยืนยันตัวตน, body ที่ไม่บังคับ และ JSON Schema สำหรับอาร์กิวเมนต์ที่โมเดลให้มา เทมเพลตหนึ่งอาจมีทั้งค่าที่โมเดลให้ เช่น หมายเลขคำสั่งซื้อ และค่าที่แพลตฟอร์มเติมให้ เช่น เบอร์โทรศัพท์ของบทสนทนาปัจจุบัน หรือ secret ที่บันทึกไว้ของเวิร์กสเปซ endpoint ระยะไกลระบุฟังก์ชันหนึ่งฟังก์ชันพร้อมสคีมาของฟังก์ชันนั้น และแพลตฟอร์มส่งต่ออาร์กิวเมนต์ของโมเดลไปยัง URL ของ endpoint โดย endpoint นี้ไม่ได้ใช้ Model Context Protocol
Simon Willison เรียกการรวมกันของข้อมูลส่วนตัว เนื้อหาที่ไม่น่าเชื่อถือ และการสื่อสารกับภายนอกว่า the lethal trifecta ของเอเจนต์ AI คอนเน็กเตอร์นำทั้งสามอย่างมารวมกัน และการควบคุมแต่ละอย่างในบทความนี้ทำให้ขาหนึ่งขาแคบลง:
| ขา | สิ่งที่เอเจนต์ถืออยู่ | การควบคุมที่ทำให้ขานี้แคบลง |
|---|---|---|
| ข้อมูลส่วนตัว | รายละเอียดของลูกค้า และสิ่งใดก็ตามที่เครื่องมือส่งกลับมา | ค่าของแพลตฟอร์มที่โมเดลให้เองไม่ได้ (ส่วนที่ 8); ขอบเขตระดับเวิร์กสเปซและระดับระเบียน (ส่วนที่ 6 และ 8); การปกปิดค่าส่วนบุคคลที่เครื่องมือส่งออกไป (ส่วนที่ 5) |
| เนื้อหาที่ไม่น่าเชื่อถือ | ข้อความของลูกค้าและการตอบกลับจาก upstream | ผลลัพธ์ที่ติดป้ายว่าเป็นข้อมูลและถูกจำกัดขนาด (ส่วนที่ 7 และ 9); ระเบียนที่เรนเดอร์ทั้งระเบียน (ส่วนที่ 10) |
| การสื่อสารกับภายนอก | เครื่องมือที่ส่งคำขอ ซึ่งบางตัวนำข้อมูลรับรองไปด้วย | การควบคุมปลายทาง (ส่วนที่ 2); การสร้างคำขอ (ส่วนที่ 3); secret ที่กำหนดขอบเขตตามการใช้งาน (ส่วนที่ 4); การทำงานอัตโนมัติที่จำกัดสิทธิ์ไว้ที่เจ้าของ (ส่วนที่ 6 และ 7) |
ผู้มีบทบาท
| ผู้มีบทบาท | ไว้ใจให้ | ไม่ไว้ใจให้ |
|---|---|---|
| ลูกค้า | สนทนากับเอเจนต์ | เลือกว่าเครื่องมือส่งข้อมูลไปที่ใด หรือเห็นระเบียนของลูกค้ารายอื่น |
| โมเดล | เลือกการเรียกเครื่องมือจากแค็ตตาล็อกที่ได้รับ | ให้ค่าที่แพลตฟอร์มเป็นผู้เติม หรือตัดสินใจแทนเจ้าของ |
| API upstream หรือ endpoint ระยะไกล | ส่งข้อมูลกลับ | รักษาให้การตอบกลับมีขนาดเล็กและมาทันเวลา กันข้อมูลรับรองออกจากสิ่งที่สะท้อนกลับมา หรือสั่งการโมเดล |
| DNS ของโฮสต์ที่เวิร์กสเปซเลือก | ไม่มี | ตอบอย่างคงเส้นคงวาหรือตอบทันเวลา |
| สมาชิกเวิร์กสเปซที่ไม่มีสิทธิ์แอดมิน | แก้ไขเครื่องมือทั่วไป | ตัดสินว่า secret ที่บันทึกไว้จะถูกส่งไปที่ใด หรือเปิดการเรียกที่ไม่มีใครเป็นผู้เลือก |
| เจ้าของหรือแอดมินของเวิร์กสเปซ | เลือก secret ปลายทาง และการทำงานอัตโนมัติ | เข้าถึงแอดเดรส loopback, private หรือ link-local หรืออ่านข้อมูลรับรองของแพลตฟอร์ม |
| เวิร์กสเปซอื่น | ไม่มี | รู้สิ่งใดก็ตามเกี่ยวกับเวิร์กสเปซนี้ |
ทรัพย์สิน
- ข้อมูลรับรองของแพลตฟอร์ม เช่น คีย์ที่แพลตฟอร์มใช้กับพาร์ตเนอร์ด้านการส่งข้อความ
- secret ของเวิร์กสเปซ ที่เครื่องมือส่งไปยัง API upstream
- ข้อมูลส่วนบุคคลของลูกค้า: เบอร์โทรศัพท์, username, ที่อยู่อีเมล, ตัวระบุภายนอก และฟิลด์กำหนดเองที่ถูกทำเครื่องหมายว่าเป็นข้อมูลส่วนบุคคล
- เครือข่ายภายใน: loopback, ช่วงแอดเดรส private ตาม RFC 1918, แอดเดรส link-local และบริการ cloud metadata
- ขีดความสามารถ: เวลาและหน่วยความจำของเวิร์กเกอร์, context window ของโมเดล และขีดจำกัดของผู้ให้บริการด้านจำนวนเครื่องมือต่อคำขอ
- ความถูกต้องของสิ่งที่เอเจนต์บอกลูกค้า
- การตัดสินใจของเจ้าของ: เมื่อเจ้าของปิดเครื่องมือ หรือเลือกว่า secret ที่บันทึกไว้จะถูกส่งไปที่ใด การตัดสินใจนั้นต้องมีผล
ความล้มเหลวเก้าประเภท
| # | ประเภทความล้มเหลว | รอยต่อ | ส่วน |
|---|---|---|---|
| 1 | การควบคุมปลายทางขาออก | จากคำขอที่เรนเดอร์แล้วสู่เครือข่าย | 2 |
| 2 | การสร้างคำขอ | จากเทมเพลตของเวิร์กสเปซสู่คำขอที่เรนเดอร์แล้ว | 3 |
| 3 | ขอบเขตของ secret | จากการตั้งค่าสู่คำขอ | 4 |
| 4 | การกักกันข้อผิดพลาดและการตอบกลับ | จากการตอบกลับและ exception สู่โมเดลและ log | 5 |
| 5 | การกำหนดสิทธิ์ตามผลกระทบ | จากคำขอตั้งค่าสู่เครื่องมือที่บันทึกไว้ | 6 |
| 6 | การเรียกอัตโนมัติ | จากการทำงานอัตโนมัติสู่การ dispatch | 7 |
| 7 | ค่าจากโมเดล เวิร์กสเปซ และแพลตฟอร์ม | จากอาร์กิวเมนต์ของโมเดลสู่คำขอ | 8 |
| 8 | ขีดจำกัดทรัพยากร | จากเครือข่ายสู่เวิร์กเกอร์; จากแค็ตตาล็อกสู่ผู้ให้บริการ | 9 |
| 9 | ความถูกต้องของผลลัพธ์ | จากการตอบกลับสู่คำตอบของโมเดล | 10 |
2. การควบคุมปลายทางขาออก
Server-side request forgery (SSRF) ในคอนเน็กเตอร์ หมายถึงการที่ URL ที่เวิร์กสเปซตั้งค่าไว้ไปถึงแอดเดรสที่ไม่ควรไปถึง ได้แก่ loopback, เครือข่าย private หรือบริการ cloud metadata ที่ 169.254.169.254 การตรวจ URL จะป้องกันได้ก็ต่อเมื่อตัดสินจากโฮสต์ แอดเดรส proxy และนโยบาย redirect ชุดเดียวกับที่ HTTP transport จะใช้ การตรวจของเราจึงตรวจอินพุตของ transport เอง
รูปแบบการเขียนโฮสต์ที่ parser แบบเข้มงวดไม่รู้จัก
FILTER_VALIDATE_IP ของ PHP ปฏิเสธ 2130706433, 0x7f000001, 0177.0.0.1 และ 127.1 guard ที่สร้างบน filter นี้จึงถือว่าค่าเหล่านี้เป็น hostname DNS ไม่คืนอะไรให้ค่าเหล่านี้ และ libcurl เชื่อมต่อไปยังทั้งสี่ค่าในฐานะ 127.0.0.1 ดังนั้น guard ที่ยอมให้ชื่อที่ได้คำตอบ DNS ว่างผ่านไปจึง fail open รูปแบบการเขียนแบบ percent-encode และแบบ zone identifier ก็มีพฤติกรรมเช่นเดียวกัน: parse_url() คืน %31%32%37.0.0.1 และ [::1%25lo0] ตามเดิม และ cURL normalise ทั้งสองค่าเป็นแอดเดรส loopback
การตรวจปลายทางของเรารับโฮสต์ก็ต่อเมื่อโฮสต์นั้นเป็น DNS hostname ธรรมดา หรือเป็น IP address ในรูปแบบ canonical และปฏิเสธ hostname ใดก็ตามที่ resolve ไม่ได้ตอน dispatch การตรวจนี้ทำงานกับ URL หลังจากเขียนทุกค่าลงไปแล้ว รวมถึงค่าของโมเดล การตรวจแอดเดรสปฏิเสธแบบทั้งบล็อก ทั้งทุกช่วงที่ทะเบียนแอดเดรส special-purpose ของ IANA สำหรับ IPv4 และ IPv6 ระบุว่าไม่ globally reachable และ multicast ซึ่งครอบคลุม loopback, 0.0.0.0/8, ช่วง private ตาม RFC 1918, shared address space 100.64.0.0/10 ที่ carrier-grade NAT และ overlay network ใช้, แอดเดรส link-local รวมถึงแอดเดรสของ cloud metadata และช่วงสำหรับเอกสารและสำหรับ benchmarking สำหรับ IPv6 เราจำกัดไว้ที่ global unicast คือ 2000::/3 โดยไม่รวมบล็อกสำหรับเอกสาร บล็อก 6to4 และบล็อก Teredo ที่อยู่ภายในช่วงนั้น แอดเดรสแบบ IPv4-mapped, IPv4-compatible, 6to4 และ Teredo ถูกปฏิเสธโดยไม่มีข้อยกเว้น ส่วนแอดเดรสใน well-known NAT64 prefix 64:ff9b::/96 จะถูกตัดสินจาก IPv4 address ที่แอดเดรสนั้นพาอยู่
นโยบายนี้เป็นตารางช่วงแอดเดรสที่ระบุชัดเจน เพราะ range flag ของ PHP ปล่อยแอดเดรสที่ไม่ใช่แอดเดรสสาธารณะผ่านไป FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ยอมรับ 100.64.0.1, 198.18.0.1, 192.0.2.1, 224.0.0.1, loopback ในรูป NAT64 คือ 64:ff9b::7f00:1 และในรูป 6to4 คือ 2002:7f00:1::1 ในทุกเวอร์ชันของ PHP ที่เราทดสอบ (8.1, 8.2 และ 8.5) และใน 8.1 กับ 8.2 ยังยอมรับ ::ffff:127.0.0.1 ด้วย shared address space ที่ flag เหล่านี้ยอมรับมีบริการ cloud metadata อยู่: บริการของ Alibaba Cloud ตอบที่ 100.100.100.200 ตารางแอดเดรส special-purpose ชุดเดียวกันนี้ขับเคลื่อนทั้ง unit test ของนโยบาย การตรวจตอนบันทึก และการตรวจตอน dispatch
แอดเดรสที่ตรวจกับแอดเดรสที่เชื่อมต่อ
guard ที่ resolve ชื่อแล้วปล่อยให้ HTTP client resolve ชื่อนั้นอีกครั้ง ได้อนุมัติคำตอบ DNS คำตอบหนึ่ง แต่เชื่อมต่อตามอีกคำตอบหนึ่ง zone ที่ทำ DNS rebinding สามารถคืนแอดเดรสสาธารณะให้การตรวจ และคืนแอดเดรส private ให้การเชื่อมต่อ ซึ่งเป็นช่องว่างแบบ time-of-check to time-of-use (CWE-367) เรา pin แอดเดรสที่อนุมัติแล้วไว้กับการเชื่อมต่อด้วย CURLOPT_RESOLVE ของ cURL เพื่อให้ socket เปิดไปยังแอดเดรสที่ guard ตรวจแล้ว
รายละเอียดสองข้อเป็นตัวตัดสินว่า pin มีผลหรือไม่:
- คีย์ของ pin ต้องตรงกับโฮสต์ของคำขอทุกตัวอักษร cURL จับคู่รายการ
CURLOPT_RESOLVEด้วยสตริงของโฮสต์ ดังนั้น pin สำหรับapi.example.testจะไม่มีผลกับคำขอไปยังชื่อแบบ fully qualifiedapi.example.test.ที่มีจุดต่อท้าย - Proxy คือ resolver ตัวที่สอง เมื่อผ่าน proxy, cURL ส่ง
CONNECT host:portและ proxy เป็นผู้ resolve ชื่อ pin จึงไม่ได้เป็นตัวตัดสินปลายทางอีกต่อไป
ปลายทางของ redirect คือ URL ที่สองที่ guard ไม่เคยเห็น HTTP client ทั้งสี่ตัวด้านล่างตาม redirect โดยค่าเริ่มต้น และสามตัวในนั้นอ่านการตั้งค่า proxy จาก environment:
| HTTP client | ใช้ proxy จาก environment โดยค่าเริ่มต้น | ตาม redirect โดยค่าเริ่มต้น |
|---|---|---|
| PHP, Guzzle 7 | ใช่: HTTPS_PROXY และ NO_PROXY รวมถึง HTTP_PROXY ใน process แบบ command line |
ใช่ สูงสุด 5 ครั้ง |
| Python, requests | ใช่: http_proxy, https_proxy, no_proxy, all_proxy และรูปตัวพิมพ์ใหญ่ของตัวแปรเหล่านี้ |
ใช่ สำหรับทุกเมธอดยกเว้น HEAD |
Go, ค่าเริ่มต้นของ net/http |
ใช่: HTTP_PROXY, HTTPS_PROXY และ NO_PROXY |
ใช่ สูงสุด 10 ครั้ง |
Node.js, fetch |
เฉพาะเมื่อตั้ง NODE_USE_ENV_PROXY=1 ซึ่งมีตั้งแต่ Node 24.0 และ 22.21 |
ใช่ |
คำขอของคอนเน็กเตอร์จึงทำงานโดยปิด redirect ไม่สนใจการตั้งค่า proxy จาก environment และ pin แอดเดรสที่ตรวจแล้ว ใน Guzzle สิ่งเหล่านี้คือ request option สามตัว:
// $host is the request's host exactly as written, trailing dot included.
$options = [
'allow_redirects' => false,
'proxy' => '', // an empty proxy overrides HTTP_PROXY and HTTPS_PROXY
'curl' => [CURLOPT_RESOLVE => ["{$host}:{$port}:{$checkedAddress}"]],
];
เอกสารของ CURLOPT_PROXY ระบุว่าสตริง proxy ที่ว่างจะปิด proxy แม้ว่าตัวแปร environment จะตั้ง proxy ไว้ก็ตาม OWASP SSRF Prevention Cheat Sheet แนะนำแนวทางเดียวกัน: ตรวจสอบปลายทาง, resolve แล้วตรวจแอดเดรสของปลายทาง และปิด redirect
โฮสต์ที่ guard อ่านคือโฮสต์ที่ cURL เชื่อมต่อ
guard ที่ดึงโฮสต์ด้วย parser ตัวหนึ่ง ขณะที่ transport ใช้อีกตัวหนึ่ง อาจอนุมัติโฮสต์หนึ่ง แล้วเชื่อมต่อไปยังอีกโฮสต์หนึ่ง payload แบบคลาสสิกซ่อนโฮสต์จริงไว้หลัง \@, #@, ?@, @ ตัวที่สอง หรือเครื่องหมายทับที่ถูก encode ใน userinfo เช่น https://allowed.example.test#@127.0.0.1/ เมื่อรันผ่าน guard ของเราและ transport ร่วมกัน payload แต่ละตัวจะถูกปฏิเสธ หรือไม่ก็เชื่อมต่อไปยังโฮสต์ที่ guard ตรวจแล้ว ทุกแอดเดรสในคำตอบ DNS ถูกตรวจช่วง และการเชื่อมต่อถูก pin ไว้กับชุดแอดเดรสที่ตรวจแล้วนั้น
DNS ต้องมี deadline
dns_get_record() ของ PHP ไม่รับค่า timeout ดังนั้น authoritative server ที่ช้าของโดเมนที่เวิร์กสเปซเลือกสามารถกักเวิร์กเกอร์ไว้นานเท่าใดก็ได้ตามที่ต้องการ การจับเวลา lookup หลังจากที่ lookup คืนค่าแล้ววัดความล่าช้าได้ แต่ไม่ได้จำกัดความล่าช้านั้น คอนเน็กเตอร์ของเรา resolve ชื่อใน process แยกที่ถูกหยุดเมื่อถึง deadline ภายในงบเวลาของเครื่องมือ และ lookup ที่ทำไม่เสร็จจะถูกปฏิเสธแบบเดียวกับ lookup ที่ล้มเหลว
Parser เอกสารคือ network client
PhpWord โหลดรูปภาพโดยค่าเริ่มต้น และสำหรับรูปภาพ VML ที่ relationship ถูกระบุว่าเป็น external ก็จะโหลดรูปภาพจาก URL ที่เอกสารระบุ ไฟล์ Word ที่อัปโหลดเข้ามาจึงทำให้เซิร์ฟเวอร์ดึงข้อมูลจากแอดเดรสใดก็ได้ที่ไฟล์นั้นเลือก ตัวอ่านเอกสารของเราดึงข้อความ ซึ่งไม่มีวันต้องใช้รูปภาพ จึงไม่โหลดรูปภาพใดเลย
เครื่องมือทั้งสองประเภทใช้ guard เดียวกัน
เครื่องมือ HTTP API และ proxy ของ endpoint ระยะไกลใช้การตรวจปลายทางเดียวกันและ transport option ชุดเดียวกัน นอกจากนี้ proxy ยังรับเฉพาะ URL แบบ absolute ที่เป็น http และ https และระบุโฮสต์ บันทึก audit ของการรันทดสอบด้วยตนเองเก็บตัวระบุของเครื่องมือแทน URL ของเครื่องมือ
3. การสร้างคำขอ
คำขอที่ผ่านการตรวจแล้วยังเปลี่ยนไปได้ก่อนถูกส่ง HTTP client บางตัวถือว่าทุก URL เป็นเทมเพลต header และ JSON body ต่างก็มีไวยากรณ์ของตัวเอง และ parser ของ query string อ่านชื่อพารามิเตอร์ต่างกัน เราเขียนทุกค่าตามตำแหน่งที่ค่านั้นอยู่ภายในรอบเดียว และปฏิเสธทุกอย่างที่ขั้นตอนถัดไปจะตีความใหม่
HTTP client ที่ expand เทมเพลต URL
HTTP client ของ Laravel รองรับ URI template และเมธอด send() ของ client นี้ส่งทุก URL ผ่าน expander ตาม RFC 6570 ของ Guzzle ไม่ว่าผู้เรียกจะ bind พารามิเตอร์ของเทมเพลตไว้หรือไม่ก็ตาม ตาม RFC 6570 §3.2.1 expression ที่ไม่มีค่าจะ expand เป็นค่าว่าง:
Template as configured: https://api.example.test/members/{member_id}
Sent when unfilled: https://api.example.test/members/
URL ที่สองขอทั้ง collection expression ที่มีตัวดำเนินการ เช่น {?member_id}, {/member_id} และ {+member_id} ก็ expand เป็นค่าว่างเช่นเดียวกัน Reserved expansion ({+var}) และ fragment expansion ({#var}) ปล่อย /, ?, # และ @ ผ่านไปโดยไม่ encode ส่วนที่อยู่ในวงเล็บปีกกาจึงเปลี่ยน path, query หรือโฮสต์ของ URL ได้ expression ที่ไม่มีค่าก็ย้ายโฮสต์ได้เช่นกัน ดังที่ตัวอย่างสาธิตกรณีแรกแสดงไว้ การตรวจที่รู้จักเฉพาะ {name} จะปล่อยทั้งหมดนี้ผ่านไป
expander ของ Guzzle คืนสตริงที่ไม่มีวงเล็บปีกกาเลยโดยไม่เปลี่ยนแปลง renderer ของเราอาศัยพฤติกรรมนี้ renderer เขียนทุกค่าเอง ต่ออาร์กิวเมนต์ที่ไม่มี placeholder ใดใช้เข้ากับ query ของ GET หรือ DELETE และหากยังมี { หรือ } หลงเหลืออยู่ ก็หยุดการเรียกก่อนที่จะมีสิ่งใดถูกส่ง ข้อผิดพลาดระบุว่าเป็นกรณีอาร์กิวเมนต์ที่ประกาศไว้ขาดหายไป มีการใช้อาร์กิวเมนต์ที่ไม่ได้ประกาศ หรือตัวเทมเพลตเองไม่ถูกต้อง
renderer เดียว รอบเดียว
renderer ตัวเดียวเขียนทุกเทมเพลตที่เครื่องมือส่ง (URL, header, body และข้อมูลรับรอง) สำหรับทุกเส้นทางที่ส่งเทมเพลตนั้น ได้แก่ การเรียกของโมเดลเอง การ pre-fetch อัตโนมัติที่อธิบายในส่วนที่ 7 และการรันทดสอบด้วยตนเอง renderer เขียนอาร์กิวเมนต์ของโมเดลและค่าของแพลตฟอร์มในรอบเดียวกัน และค่าที่เขียนไปแล้วจะไม่มีวันถูกอ่านซ้ำ ค่าที่เขียนแล้วจึงไม่สามารถเติม placeholder ให้สมบูรณ์ได้ และอาร์กิวเมนต์ก็ไม่สามารถเปลี่ยน {{secret:{key}}} ให้กลายเป็นการอ้างอิง secret ที่โมเดลเป็นผู้เลือก
ค่าที่เขียนตามตำแหน่งที่ค่านั้นอยู่
| ตำแหน่งของค่า | วิธีเขียน | ถูกปฏิเสธ |
|---|---|---|
| URL | Percent-encode รวมถึง secret ที่บันทึกไว้ | placeholder ที่ไม่ได้ประกาศ แม้การเรียกจะส่งค่านั้นมา; ค่าที่หายไปหรือว่าง; ค่าที่ก่อให้เกิด path segment . หรือ .. |
| Header | ตามที่เป็น | อาร์กิวเมนต์ที่มีการขึ้นบรรทัดใหม่ อักขระควบคุมอื่นนอกจาก tab หรือข้อความที่ไม่ใช่ ASCII; placeholder ที่ไม่ได้ประกาศ; ค่าที่หายไปหรือว่าง |
| Body ภายใน JSON string | เป็นเนื้อหาของ JSON string โดย escape เฉพาะจุดที่ JSON กำหนด | อาร์กิวเมนต์ที่ประกาศไว้แต่การเรียกไม่ได้ส่งมา |
| Body ที่ตำแหน่งค่า | เป็นค่า JSON ที่โมเดลส่งมา: number, boolean, null, array หรือ object | อาร์กิวเมนต์ที่ประกาศไว้แต่การเรียกไม่ได้ส่งมา |
Percent-encoding ไม่แตะ . ค่าหนึ่งจึงยังก่อให้เกิด dot segment ใน path ของ URL ได้ ซึ่ง libcurl จะลบออกก่อนส่ง (ตัวอย่างสาธิตกรณีที่สี่) renderer ติดตามว่าอักขระใดใน path มาจากค่า และปฏิเสธ segment . หรือ .. ที่ค่าก่อให้เกิด ไม่ว่าค่านั้นจะมาจากโมเดล แพลตฟอร์ม หรือ secret ที่บันทึกไว้ dot segment ที่ operator เขียนไว้ในเทมเพลตจะถูกส่งตามที่เขียน: นั่นคือ path ของ operator เองบนโฮสต์ของ operator เอง
การ percent-encode ค่าใน body ทำให้ upstream อ่านข้อความที่ถูก encode ตามตัวอักษร: Tan Ah Kow ไปถึงในรูป Tan%20Ah%20Kow และการค้นหาด้วยชื่อนั้นไม่พบอะไรเลย การเขียนข้อความดิบลงใน body เปิดทางให้เครื่องหมายอัญประกาศเพิ่มคีย์ได้ ค่าของแพลตฟอร์มใน body ต้องอยู่ภายใน JSON string ซึ่งเครื่องหมายอัญประกาศในชื่อของผู้ติดต่อจะถูก escape โดยค่าที่ไม่บังคับซึ่งอธิบายด้านล่างเป็นข้อยกเว้นเพียงข้อเดียว body ที่ไม่เป็น JSON ที่ถูกต้องเมื่อเขียนทุกค่าลงไปแล้ว หรือที่วางค่าไว้ในคีย์ของ object จะถูกปฏิเสธตอนบันทึกเครื่องมือ และถูกปฏิเสธอีกครั้งตอนรัน อาร์กิวเมนต์ที่ JSON แสดงไม่ได้ เช่น 1e999 จากโมเดล ซึ่ง decode ได้เป็น infinity ทำให้การเรียกถูกปฏิเสธในทุกช่องทาง ใน body วงเล็บปีกกาที่สคีมาไม่ได้ประกาศจะถูกส่งตามที่เขียน เนื่องจาก selection set ของ GraphQL เขียนด้วยวงเล็บปีกกา
ชื่อพารามิเตอร์ที่มีความหมายเดียวกัน
อาร์กิวเมนต์ของโมเดลที่ไม่มี placeholder ใดใช้จะถูกต่อท้ายเข้าไปใน query renderer จึงต้องไม่ให้อาร์กิวเมนต์ตัวใดชนกับพารามิเตอร์ที่ operator กำหนดไว้ตายตัวในเทมเพลต เช่น ตัวกรอง tenant parse_str() ของ PHP ใช้เปรียบเทียบแบบนั้นไม่ได้: ฟังก์ชันนี้แปลงจุดและช่องว่างในชื่อเป็นขีดล่าง และทำให้ชื่อที่มีวงเล็บเหลี่ยมกลายเป็นโครงสร้างซ้อน tenant.id จึงถูกอ่านเป็น tenant_id และ filter[tenant_id] ถูกอ่านเป็น filter เราเปรียบเทียบชื่อด้วยอัตลักษณ์เดียวที่รวมทุกการตีความเข้าด้วยกัน: ตัดที่ [ ตัวแรก แปลง . และช่องว่างเป็น _ และไม่สนใจตัวพิมพ์เล็กใหญ่ อาร์กิวเมนต์ของโมเดลที่อัตลักษณ์ตรงกับพารามิเตอร์ที่มีอยู่แล้วใน URL ที่เรนเดอร์จะถูกตัดทิ้ง และค่าของ operator ยังคงอยู่
ตอนบันทึกกับตอนรันให้ผลตรงกัน
ทุก placeholder ใน URL หรือ header ที่ส่งของเครื่องมือต้องถูกประกาศในสคีมาอาร์กิวเมนต์และระบุว่าเป็น required ก่อนจึงจะบันทึกเครื่องมือได้ และทุก placeholder ที่ประกาศไว้ใน body ต้องเป็น required ตอนรัน การเรียกที่ขาดค่าใดค่าหนึ่งเหล่านี้จะถูกปฏิเสธก่อนมีสิ่งใดถูกส่ง มิฉะนั้น placeholder ที่ไม่บังคับจะหายไปจากคำขอ และคำขอก็จะชี้ไปยัง resource อื่นที่ไม่ใช่ resource ที่ operator อธิบายไว้
Header ที่คำขอนำไปด้วย
การเรียกหนึ่งครั้งจะส่ง header ใดบ้างถูกตัดสินเพียงครั้งเดียวจากการตั้งค่าของเครื่องมือ ก่อนที่จะ resolve ค่าใด ๆ ได้แก่ header ของเครื่องมือเอง, Authorization ของโหมดการยืนยันตัวตน, Content-Type หนึ่งตัวเมื่อเมธอดส่ง body, Accept หนึ่งตัว และ header ของ transport เอง เช่น Host การตรวจที่ตัดสินว่าการเรียกจะออกไปได้หรือไม่ (ค่าที่หายไป, รั้วกั้น secret ในส่วนที่ 4, ข้อกำหนดของ pre-fetch ในส่วนที่ 7) อ่านผลการตัดสินเดียวกันนั้น มี test ที่จะล้มเหลวหาก HTTP client ที่แอปพลิเคชันใช้ร่วมกันได้รับ default option หรือ middleware ใดเพิ่มขึ้น เพราะเครื่องมือของ tenant จะพาสิ่งนั้นไปยังโฮสต์ที่ tenant เลือก
ค่าของแพลตฟอร์มที่ไม่บังคับ
การค้นหาบางแบบรับตัวระบุตัวใดตัวหนึ่งจากหลายตัว เช่น เบอร์โทรศัพท์หรือ username ค่าของแพลตฟอร์มที่ระบุว่าไม่บังคับวางได้เฉพาะที่ตำแหน่งค่าของ JSON ใน body เท่านั้น ค่านี้ถูกเขียนเป็น JSON string เมื่อมีค่า และเป็น null เมื่อไม่มีค่า และจะถูกปฏิเสธเมื่ออยู่ใน URL, header, ข้อมูลรับรอง, ภายในเครื่องหมายอัญประกาศ และเมื่อเป็นคีย์ของ object secret ไม่มีวันเป็นค่าที่ไม่บังคับ การเรียกจะถูกปฏิเสธเมื่อค่าที่ไม่บังคับหายไปทุกค่า และไม่มีค่าอื่นจากบทสนทนา ผู้ติดต่อ หรือฟิลด์กำหนดเองไปถึงคำขอ การค้นหาจึงไม่มีวันถูกส่งออกไปโดยไม่มีสิ่งใดที่ระบุตัวลูกค้า
4. ขอบเขตของ secret
secret ที่บันทึกไว้จะปลอดภัยก็ต่อเมื่อคำถามสองข้อมีคำตอบเดียวกัน คือ ใครมีสิทธิ์จัดการ secret นั้น และใครมีสิทธิ์ตัดสินว่า secret นั้นถูกส่งไปที่ใด StaffOS เก็บ secret ที่บันทึกไว้ของแต่ละเวิร์กสเปซในที่เก็บที่เข้ารหัสของเวิร์กสเปซนั้นเอง resolve การอ้างอิง secret จากเวิร์กสเปซของเครื่องมือเองเท่านั้น และจำกัดทุกการเปลี่ยนแปลงที่อาจเปลี่ยนปลายทางของ secret ที่บันทึกไว้ให้ทำได้เฉพาะคนที่มีสิทธิ์จัดการ secret นั้น
ที่เก็บเดียวที่มีขอบเขต
การอ้างอิง secret ในเครื่องมือ resolve จากที่เก็บของเวิร์กสเปซของเครื่องมือเอง และไม่ resolve จากที่อื่นใด การอ้างอิง secret ที่เวิร์กสเปซยังไม่ได้บันทึกจะหยุดการเรียก และไม่มีสิ่งใดถูกส่งออกไป
การจัดการ secret กับการใช้ secret เป็นสิทธิ์คนละอย่าง
การจำกัดว่าใครสร้าง secret ได้ยังเหลือเส้นทางที่สองเปิดอยู่ สมาชิกที่อ่าน secret ที่บันทึกไว้ไม่ได้ อาจแก้ไขเครื่องมือที่ส่ง secret นั้น ชี้เครื่องมือไปยังโฮสต์ที่ตนควบคุม แล้วรันทดสอบ นี่คือ confused deputy ของ Norm Hardy: แพลตฟอร์มถืออำนาจไว้ และฝ่ายที่ไม่ควรใช้อำนาจนั้นเป็นผู้เลือกว่าจะใช้อย่างไร
สำหรับเครื่องมือที่ส่ง secret ที่บันทึกไว้ มีเพียงเจ้าของหรือแอดมินของเวิร์กสเปซเท่านั้นที่ทำสิ่งต่อไปนี้ได้:
- เปลี่ยนเมธอด, URL, header, การยืนยันตัวตน หรือ body ของเครื่องมือ
- รันเครื่องมือด้วยตนเองจากหน้าการตั้งค่า
- เปิดเครื่องมือกลับมาใช้งานหลังจากถูกปิดไปแล้ว
เราจำกัดสิทธิ์เฉพาะสิ่งที่บทบาทระดับต่ำกว่าทำซ้ำด้วยวิธีอื่นไม่ได้ การรันทดสอบด้วยตนเองของเครื่องมือที่ไม่ได้ส่ง secret ที่บันทึกไว้เปิดให้ทุกบทบาท เพราะด่านนี้ไม่ได้ช่วยอะไรเมื่อสมาชิกแก้ไขหรือคัดลอกเครื่องมือแบบนั้นได้อยู่แล้ว
ทำไมการเขียนในรอบเดียวจึงสำคัญสำหรับ secret
เทมเพลตคำขอหนึ่งอาจมีทั้ง secret ที่บันทึกไว้ ค่าของบทสนทนา ค่าของผู้ติดต่อ และอาร์กิวเมนต์ของโมเดลปนกัน การ resolve ค่าเหล่านี้หลายรอบแยกกันเปิดทางให้ค่าหนึ่งแทรกค่าอื่นเข้ามา หากแทนค่า secret ก่อน แล้วสแกนผลลัพธ์เพื่อหา placeholder ของบทสนทนา secret ที่ข้อความบังเอิญมี placeholder ของบทสนทนาอยู่จะดึงเบอร์โทรศัพท์ของลูกค้าเข้ามาในคำขอ การสลับลำดับย้ายข้อบกพร่องเดียวกันไปอยู่ที่ค่าของบทสนทนาที่มีข้อความรูปแบบเหมือน secret การเขียนในรอบเดียวตามส่วนที่ 3 อ่านเฉพาะข้อความต้นฉบับของ operator ค่าที่แทนเข้าไปแล้วจึงไม่มีวันถูก parse เป็น placeholder
ปฏิเสธสิ่งที่ resolve ไม่ได้
การอ้างอิงที่ไม่ตรงกับ pattern ใด เช่น placeholder ที่สะกดผิดหรือไม่มีตัวปิด ต้องไม่ถูกส่งต่อไปเป็นข้อความตามตัวอักษรใน header, body หรือข้อมูลรับรอง pattern ชั้นนอกที่หลวมหาทุกช่วงข้อความที่เข้าข่าย pattern ชั้นในที่เข้มงวดตรวจความถูกต้องของช่วงนั้น และช่วงใดก็ตามที่ยัง resolve ไม่ได้จะทำให้การเรียกถูกปฏิเสธ
ข้อมูลรับรองก็ fail closed เช่นกัน secret ที่ resolve แล้วไม่ได้ค่าที่ใช้งานได้, secret ที่ยังไม่ได้บันทึกใน bearer token หรือโหมดการยืนยันตัวตนที่ไม่รู้จัก จะหยุดการเรียกก่อนถูกส่ง ทางเลือกอื่นคือคำขอที่ไม่มีการยืนยันตัวตน และ upstream ที่รับการเขียนแบบไม่ระบุตัวตนจะทำให้เกิดผลข้างเคียงนั้น
ข้อมูลรับรองที่แพลตฟอร์มถอดรหัสไม่ได้
ข้อมูลรับรองที่เข้ารหัสด้วยคีย์ที่แพลตฟอร์มไม่มีแล้วจะอ่านไม่ได้ และไม่มีสิ่งใดตรวจได้ว่าข้อมูลรับรองนั้นจะส่งอะไรออกไป เครื่องมือที่ถอดรหัสข้อมูลรับรองไม่ได้จะหลุดออกจากแค็ตตาล็อกของเอเจนต์ และ secret ที่บันทึกไว้ซึ่งถอดรหัสไม่ได้จะ resolve แล้วไม่ได้ค่าใด ซึ่งทำให้การเรียกถูกปฏิเสธ สำหรับรั้วกั้นเจ้าของหรือแอดมิน เครื่องมือแบบนี้นับว่าส่ง secret ที่บันทึกไว้ การบันทึกตามปกติจะคง ciphertext ที่เก็บไว้ คีย์ที่กู้คืนมาจึงยังอ่านได้ มีเพียงค่าที่มีคนป้อนเข้ามา หรือการเลือกลบอย่างชัดแจ้งเท่านั้นที่จะแทนที่ ciphertext นั้น ในจุดที่การอ่านแสดง placeholder แทนข้อมูลรับรองที่เก็บไว้ การเขียนจะปฏิเสธ placeholder นั้นในฐานะค่า การทำ read-modify-write จึงไม่สามารถแทนที่ข้อมูลรับรองจริงด้วยค่า mask ของข้อมูลรับรองนั้นได้
ข้อมูลรับรองที่เก็บไว้เพื่อวัตถุประสงค์หนึ่ง
ข้อมูลรับรองที่เก็บไว้เพื่อวัตถุประสงค์หนึ่งต้องไม่ถูกเลือกไปใช้เพื่อวัตถุประสงค์อื่น proxy ของ endpoint ระยะไกลยืนยันตัวตนด้วยข้อมูลรับรองที่เวิร์กสเปซเก็บไว้ได้ และจะปฏิเสธการตั้งค่าการยืนยันตัวตนที่ระบุผู้ให้บริการโมเดล AI รายใดรายหนึ่งของเวิร์กสเปซ ก่อนที่จะอ่านข้อมูลรับรองใด ๆ ไม่มีการตั้งค่าเครื่องมือใดที่ส่งคีย์โมเดลที่เก็บไว้ไปยังโฮสต์ที่ผู้ตั้งค่าเลือกได้
Secret ใน log
การแลกเปลี่ยน code ของ OAuth บางแบบใส่ client secret ไว้ใน query string Meta ระบุในเอกสารว่าการแลกเปลี่ยน token ของ WhatsApp Embedded Signup เป็น GET ไปยัง /oauth/access_token โดยมี client_secret อยู่ในพารามิเตอร์ของ query และ HTTP client มักใส่ URL เต็มไว้ในข้อผิดพลาดของ transport สตริงของ exception ใน PHP ยังรวมข้อความของ exception ก่อนหน้าทุกตัวใน chain ด้วย และสตริงนั้นคือสิ่งที่ error log และบันทึก failed job เก็บไว้ การเรียก Meta API ของเราที่นำข้อมูลรับรองไปใน query string จะ throw ความล้มเหลวของ transport ใหม่ โดยลบ query string ทั้งหมดออก และไม่แนบ exception ต้นฉบับไปด้วย
5. การกักกันข้อผิดพลาดและการตอบกลับ
ข้อความข้อผิดพลาดและ body ของการตอบกลับคือสองเส้นทางที่ข้อมูลรับรองหลุดออกจากการเรียกเครื่องมือ สำหรับความล้มเหลวของ transport เราคืนการจัดประเภทและประโยคตายตัว และทิ้งข้อความของไลบรารี สำหรับการตอบกลับของเครื่องมือทั้งสองประเภท เราปกปิดข้อมูลรับรองที่เครื่องมือส่งออกไปในทุกรูปแบบการเขียนที่ JSON อนุญาต และเครื่องมือ HTTP API ยังปกปิดค่าส่วนบุคคลที่ส่งออกไปด้วย
แทนที่ข้อความข้อผิดพลาดแทนการ scrub
ไลบรารี HTTP ยกอินพุตที่ทำให้เกิดข้อผิดพลาดมาแสดง libcurl ใส่ hostname ซ้ำในข้อความอย่าง Could not resolve host ข้อมูลรับรองที่อยู่ใน hostname จึงรอดพ้นการ scrub URL ทุกแบบ การตรวจ header ของ Guzzle throw exception ที่ยกค่า header ที่ไม่ถูกต้องมาแสดง ตัว scrub ต้องคาดการณ์ทุกตำแหน่งที่ข้อมูลรับรองอาจอยู่ (พารามิเตอร์ของ query ภายใต้ชื่อและตัวพิมพ์ใดก็ได้, คีย์ที่ซ้ำและคีย์ที่มีวงเล็บเหลี่ยม, userinfo, path segment, hostname, ตัวเลข) และทุกไลบรารีที่อาจยกข้อมูลรับรองนั้นมาแสดง
เราไม่ scrub คำขอที่ล้มเหลวในชั้น transport จะคืนการจัดประเภทหนึ่งจากชุดเล็ก ๆ และประโยคตายตัวที่ไม่มีสิ่งใดที่ได้มาจากคำขอหรือ exception โค้ดเพียงส่วนเดียวที่อ่าน exception ตัดสินสองเรื่อง: ความล้มเหลวนั้นเป็น timeout หรือไม่ โดยใช้หมายเลขข้อผิดพลาดของ cURL เมื่อมี และการตอบกลับมีขนาดใหญ่เกินไปหรือไม่
บังคับการกักกันแบบ static
architecture test ตัวหนึ่ง parse ทุกเครื่องมือที่ส่งคำขอ HTTP และกำหนดให้ทุกการเรียกที่ไปถึงเครือข่ายต้องอยู่ภายใน body ของบล็อก try ที่มี handler ซึ่ง catch และกักกันความล้มเหลวของการเชื่อมต่อ test นี้ทำงานบน syntax tree, resolve ชื่อที่ import และชื่อ alias, ติดตาม HTTP client ที่ส่งเข้ามาผ่าน dependency injection และถือว่ารูปแบบโค้ดใดก็ตามที่อ่านไม่ได้เป็นการละเมิด เครื่องมือใหม่ที่มีคำขอซึ่งไม่มีการป้องกันจะทำให้ test suite ล้มเหลว
handler ต้อง catch คลาสที่ถูกต้อง ใน Laravel ConnectionException และ RequestException เป็นคลาสพี่น้องภายใต้ HttpClientException handler สำหรับ RequestException จึงไม่มีวันเห็นความล้มเหลวของ DNS หรือ timeout
ปกปิดค่าที่ทราบอยู่แล้วในทุกรูปแบบการเขียน
เมื่อการตอบกลับต้องไปถึงโมเดล คำถามจะเปลี่ยนจาก "อะไรอาจเป็นข้อมูลรับรอง?" เป็น "ข้อมูลรับรองที่ทราบอยู่แล้วนี้ปรากฏที่ไหน?" เครื่องมือ HTTP API ให้ค่าที่เครื่องมือถือว่าเป็นข้อมูลรับรอง ได้แก่ secret ที่บันทึกไว้ ข้อมูลรับรองในการตั้งค่าการยืนยันตัวตน และค่าของ header ที่ชื่อบ่งว่าเป็นข้อมูลรับรอง เช่น Authorization เครื่องมือยังเพิ่มค่าส่วนบุคคลของลูกค้าที่ส่งออกไป ซึ่งอธิบายด้านล่าง ในแต่ละรูปแบบการเขียนที่ส่งออกไป รวมถึงรูปแบบการเขียน JSON ใน body proxy ของ endpoint ระยะไกลให้ชุดค่าของตัวเอง ได้แก่ ข้อมูลรับรองที่เก็บไว้, ค่าของ header ที่ชื่อบ่งว่าเป็นข้อมูลรับรอง, header Cookie และค่า cookie ที่ยาวภายใน header นั้น และส่วนต่าง ๆ ของ URL ที่อาจมีคีย์อยู่ โดยแต่ละค่าทั้งในรูปที่เขียนไว้และในรูปที่ decode แล้ว เครื่องมือทั้งสองส่งชุดค่าของตนให้ตัวปกปิดข้อมูลตัวเดียวกัน ซึ่งค้นหาแต่ละค่าในทุกรูปแบบการเขียนที่การตอบกลับอาจใช้
| รูปแบบการเขียน | ทำไมการจับคู่ระดับไบต์จึงพลาด | วิธีจัดการ |
|---|---|---|
| JSON escape | RFC 8259 §7 อนุญาตให้ escape อักขระใดก็ได้ sk\u002dlive จึงไม่มี sk-live อยู่ |
Decode body ปกปิดค่าที่ decode แล้ว แล้ว encode กลับ |
| จำนวนเต็มขนาดใหญ่ | PHP decode จำนวนเต็มที่เกิน PHP_INT_MAX เป็น float เว้นแต่จะตั้ง JSON_BIGINT_AS_STRING ไว้ ทำให้หลักที่การจับคู่ใช้เปรียบเทียบสูญหายไป |
Decode จำนวนเต็มขนาดใหญ่เป็นสตริง |
| Float | การแปลง float เป็นสตริงของ PHP พิมพ์เลขนัยสำคัญ 14 หลักโดยค่าเริ่มต้น 1234567890123456 จึงกลายเป็น 1.2345678901235E+15 |
เปรียบเทียบตัวเลขด้วยค่า canonical |
| คีย์ของ object | upstream อาจสะท้อนข้อมูลรับรองกลับมาในรูปคีย์ | ตรวจทั้งคีย์และค่า |
| Basic authentication | header มี base64 ของ user:password และ upstream ที่ parse header นี้แล้วส่งรหัสผ่านแบบข้อความธรรมดากลับมา |
ถือว่าทั้ง token ที่ encode แล้วและรหัสผ่านที่ decode แล้วเป็นค่าที่ทราบอยู่แล้ว |
| ค่าที่ซ้อนทับกัน | str_replace() ที่ใช้กับ array อาจเขียนทับข้อความที่การแทนที่ครั้งก่อนใส่เข้าไป ทำให้ marker เสียหาย |
ใช้ strtr() ซึ่งลองคีย์ที่ยาวที่สุดก่อน และไม่มีวันสแกนข้อความที่แทนที่แล้วซ้ำ |
| การซ้อนลึก | JSON body ที่ซ้อนลึกเกินขีดจำกัดความลึกของ decoder จะ decode ไม่ได้ และการถอยกลับไปใช้การจับคู่ระดับไบต์จะเปิดช่องเลี่ยงด้วย escape ขึ้นมาอีกครั้ง | คืนประโยคตายตัว |
การปกปิดต้องไม่ทำให้ข้อมูลเสียหาย การจับคู่ตัวเลขแบบ substring จะแทนที่ค่าที่ไม่เกี่ยวข้อง เช่น จำนวนนับ 10 และข้อมูลรับรองที่มีอักขระเดียวอาจเขียนทับข้อความที่ตัวจัดประเภท timeout อ่าน ตัวเลขถูกจับคู่ด้วยค่าที่ตรงกันทุกประการ และ timeout ถูกจัดประเภทจากหมายเลขข้อผิดพลาดของ cURL
ข้อมูลส่วนบุคคลในผลลัพธ์ของเครื่องมือ
เมื่อเครื่องมือ HTTP API ส่งที่อยู่อีเมลของลูกค้า ตัวระบุภายนอก หรือฟิลด์กำหนดเองที่ถูกทำเครื่องหมายว่าเป็นข้อมูลส่วนบุคคลไปยัง upstream ค่าเหล่านั้นจะถูกปกปิดออกจากการตอบกลับก่อนที่การตอบกลับจะไปถึงบริบทของโมเดลและ trace ที่จัดเก็บไว้ ชื่อและตัวระบุระเบียนของลูกค้าไม่ถูกปกปิด เพราะผลลัพธ์แสดงค่าเหล่านี้ซ้ำด้วยเหตุผลทั่วไป และการปกปิดค่าที่สั้นจะทำให้ทุกตำแหน่งที่ค่านั้นปรากฏเสียหาย
6. การกำหนดสิทธิ์ตามผลกระทบ
สิทธิ์ที่ผูกไว้กับฟิลด์ของฟอร์มอาจไม่ครอบคลุมผลกระทบที่ฟิลด์นั้นมี เราระบุสิ่งที่ความปลอดภัยของสวิตช์หนึ่งขึ้นอยู่กับ (ปลายทาง, payload, เมธอด) ตั้งรั้วกั้นการเปลี่ยนแปลงอินพุตเหล่านั้นขณะที่สวิตช์เปิดอยู่ จำกัดสิทธิ์การเปิดใช้งานออบเจกต์ และจำกัดสิทธิ์การใช้งานแบบ on-demand เราตรวจสอบแถวที่การบันทึกจะเก็บไว้ ซึ่งอาจต่างจากคำขอที่เข้ามา
จำกัดสิทธิ์ที่ผลกระทบ
การตั้งค่า pre-fetch ที่อธิบายในส่วนที่ 7 เปลี่ยนเครื่องมือให้กลายเป็นการเรียกที่ไม่มีใครเป็นผู้เลือก สำหรับเครื่องมือ POST การ pre-fetch ยังต้องมีการรับรองว่า endpoint นั้นทำเพียงการอ่านด้วย มีเพียงเจ้าของหรือแอดมินของเวิร์กสเปซเท่านั้นที่เปิดการตั้งค่าใดการตั้งค่าหนึ่งในสองอย่างนี้ได้ และใครก็ปิดได้ การจำกัดสิทธิ์ที่การตั้งค่าอย่างเดียวจะเหลือช่องโหว่ เพราะสมาชิกอาจไม่แตะการตั้งค่าเหล่านั้นเลย แต่เปลี่ยนเป้าหมายของเครื่องมือที่อยู่เบื้องหลังการตั้งค่านั้น ขณะที่เครื่องมือทำงานแบบอัตโนมัติ รั้วกั้นเจ้าของหรือแอดมินยังครอบคลุมฟิลด์คำขอของเครื่องมือด้วย การเปิดเครื่องมืออัตโนมัติที่ถูกปิดไว้กลับมาใช้งานก็ถูกจำกัดสิทธิ์ด้วยเหตุผลเดียวกัน: การตั้งค่า pre-fetch ที่เก็บไว้จะทำให้การเรียกอัตโนมัติกลับมาทำงานทันทีที่เครื่องมือถูกเปิดใช้
การมองเห็นระเบียนแคบกว่า tenancy
การรันทดสอบด้วยตนเองอาจดึงค่ามาจากบทสนทนาจริง การตรวจว่าบทสนทนาอยู่ในเวิร์กสเปซเดียวกันเป็นการพิสูจน์ tenancy ขณะที่ inbox แสดงให้ผู้ใช้แต่ละคนเห็นเฉพาะบทสนทนาที่ตนมีสิทธิ์เห็น endpoint สำหรับทดสอบ resolve บทสนทนาผ่านขอบเขตการมองเห็นเดียวกับ inbox ผู้ใช้จึงไม่สามารถระบุบทสนทนาที่ซ่อนไว้ของเพื่อนร่วมงานเพื่อยืมรายละเอียดของลูกค้าในบทสนทนานั้นได้
ตรวจสอบแถวที่การบันทึกจะเก็บไว้
การอัปเดตที่ละฟิลด์ไว้จะคงค่าที่เก็บไว้ การตรวจสอบที่อ่านเฉพาะคำขอจึงตัดสินแถวที่จะไม่มีวันเกิดขึ้นจริง Laravel ไม่รัน rule ใดเลย รวมถึง custom rule กับ attribute ที่ไม่มีอยู่หรือเป็นสตริงว่าง เว้นแต่ rule นั้นจะเป็นแบบ implicit ลองพิจารณา rule ข้ามฟิลด์ที่ยอมให้เฉพาะแอดมินเปลี่ยนเป้าหมายของเครื่องมือที่ส่ง secret หาก rule อ่านจากคำขอ การอัปเดตแบบบางส่วนที่ส่ง URL ใหม่และละข้อมูลรับรองไว้จะย้ายข้อมูลรับรองที่เก็บไว้ไปยังโฮสต์ใหม่
ก่อนการตรวจสอบจะทำงาน เส้นทางอัปเดตของเราจะคืนค่าทุกฟิลด์ที่เก็บไว้ซึ่ง rule ข้ามฟิลด์อ่านหรือผูกอยู่ rule จึงตัดสินแถวในสภาพที่จะถูกบันทึก ทุกเส้นทางที่บันทึกเครื่องมือตรวจสอบผ่าน rule ชุดเดียวกัน
7. การเรียกอัตโนมัติ
Context pre-fetch เรียกเครื่องมือที่เลือกไว้ก่อนเทิร์นของโมเดล เอเจนต์จึงเริ่มต้นด้วยข้อเท็จจริงที่มิฉะนั้นจะต้องค้นหาหรือเดาเอง นโยบายเครื่องมือของโมเดลยอมให้มีการเขียนข้อมูลจริงบางอย่าง เพราะโมเดลเลือกการเรียกนั้นตามบริบท การ pre-fetch ทำงานทุกครั้งที่มีข้อความขาเข้า และทำงานอีกครั้งเมื่อ job ลองใหม่ การเขียนข้อมูลจึงจะเกิดซ้ำโดยไม่มีใครตัดสินว่าควรเกิด pre-fetch จึงมีเกณฑ์คุณสมบัติของตัวเอง ซึ่งใช้ตอนบันทึกเครื่องมือ และใช้อีกครั้งก่อนการเรียกทุกครั้ง
เครื่องมือจะมีคุณสมบัติสำหรับ pre-fetch ก็ต่อเมื่อเป็นไปตามเงื่อนไขทั้งหมดต่อไปนี้:
- เป็น GET หรือเป็น POST ที่เจ้าของหรือแอดมินของเวิร์กสเปซรับรองแล้วว่าทำเพียงการอ่าน
- สคีมาอาร์กิวเมนต์ปิด: ไม่มี properties ไม่มี pattern properties ไม่มีอะไรเป็น required และตั้ง
additionalPropertiesเป็นfalse - ไม่มี placeholder ของอาร์กิวเมนต์ปรากฏใน URL, header ที่ส่ง หรือ body ของเครื่องมือ
POST ที่ผ่านการรับรองมีไว้สำหรับ API แบบอ่านที่รับข้อมูลส่วนบุคคล หากแปลงเป็น GET เบอร์โทรศัพท์ของลูกค้าจะเดินทางไปใน URL ซึ่ง gateway และ access log จะเก็บไว้
การรับรองทำให้ POST มีคุณสมบัติโดยไม่ลดระดับความเสี่ยงของ POST นั้น ด่านอื่นของแพลตฟอร์มจึงยังมีผล การเรียกจะถูกปฏิเสธใน sandbox ที่ยังไม่ได้เปิดการเขียนข้อมูลภายนอก ในการรันประเมินผล และในระหว่างที่บทสนทนามี support ticket เปิดอยู่
pre-fetch ทำงานจาก snapshot เดียวของเทิร์น ตัวเลือกของ pre-fetch คือสำเนาของแถวเครื่องมือในแค็ตตาล็อกที่โมเดลได้รับในเทิร์นนั้น แต่ละตัวถูกตรวจเทียบกับเวิร์กสเปซและผ่านด่านเดียวกับการเรียกของโมเดล การแก้ไขระหว่างเทิร์นจึงไม่สามารถเปลี่ยน GET ที่อยู่ในแค็ตตาล็อกให้กลายเป็น POST ได้ แต่ละเทิร์นมีเพดานจำนวนเครื่องมือ มี deadline สำหรับการเรียกแต่ละครั้งและสำหรับทั้งขั้นตอน และมีงบไบต์สำหรับบริบท เมื่อใช้งบหมดแล้วจะไม่มีการเรียกเพิ่มอีก
ผลลัพธ์ไปถึงโมเดลภายในตัวคั่นและถูกติดป้ายว่าเป็นข้อมูล การควบคุมที่สำคัญไม่ได้ขึ้นอยู่กับว่าโมเดลจะเคารพป้ายนั้นหรือไม่: แค็ตตาล็อกของเทิร์น ช่องที่โมเดลเติมได้ และทุกค่าที่แพลตฟอร์มให้ ถูกกำหนดตายตัวโดยการตั้งค่าที่โมเดลแก้ไขไม่ได้
8. ค่าจากโมเดล เวิร์กสเปซ และแพลตฟอร์ม
โมเดลเลือกอาร์กิวเมนต์ แพลตฟอร์มเติมค่าต่าง ๆ เช่น เบอร์โทรศัพท์ของลูกค้าปัจจุบัน แหล่งที่มาทั้งสองต้องไม่มีวันซ้อนทับกัน เพราะมิฉะนั้นโมเดลที่ถูก prompt injection จะเป็นผู้เลือกว่าการค้นหาใช้ข้อมูลของใคร
- สคีมาที่ชื่อชนกัน สคีมาเครื่องมือที่มีอาร์กิวเมนต์ชื่อซ้ำกับฟิลด์ของบทสนทนาหรือผู้ติดต่อที่แพลตฟอร์มเติมให้ จะถูกปฏิเสธตอนบันทึก และถูกปฏิเสธอีกครั้งตอนสร้างเครื่องมือสำหรับเทิร์น secret ที่บันทึกไว้ไม่มีวันถูกอ่านจากอาร์กิวเมนต์ จึงไม่มีสคีมาใดใช้แทน secret ได้
- ขอบเขตของเวิร์กสเปซ ค่าของผู้ติดต่อ รวมถึงเบอร์โทรศัพท์หรือ username บนอัตลักษณ์ช่องทางของบทสนทนา จะถูกอ่านเฉพาะเมื่อค่าเหล่านั้นเป็นของเวิร์กสเปซของบทสนทนาเท่านั้น
- ค่าอัตลักษณ์ผูกกับแพลตฟอร์มของตน username ไม่ซ้ำกันเฉพาะภายในแอปส่งข้อความของตัวเองเท่านั้น เครื่องมือจึงได้รับ username ของบทสนทนาเฉพาะเมื่ออัตลักษณ์อยู่บนแพลตฟอร์มที่ handle ระบุตัวผู้ส่งได้ เมื่อผู้ส่งเลิกใช้ username อัตลักษณ์จะล้างค่านั้นออก เพราะคนอื่นอาจนำ username นั้นไปใช้ในภายหลัง ข้อความเก่าที่ถูกประมวลผลล่าช้าไม่สามารถนำ username กลับมาได้: อัตลักษณ์เก็บ username จากข้อความล่าสุดของตน โดยเปรียบเทียบภายใน database update ครั้งเดียว
- ชื่อที่สงวนไว้ เครื่องมือของเวิร์กสเปซหรือ endpoint ระยะไกลที่ตั้งชื่อเหมือนเครื่องมือของแพลตฟอร์มจะบดบังเครื่องมือนั้น หรือไม่ก็ถูกตัดทิ้ง ชื่อเครื่องมือทุกชื่อที่แพลตฟอร์มอาจเสนอในบทสนทนากับลูกค้าถูกสงวนไว้ และ census test จะล้มเหลวเมื่อเครื่องมือใหม่ของแพลตฟอร์มไม่อยู่ในรายการ pattern ของชื่อลงท้ายด้วย
\zเพราะใน PCRE$ยัง match ก่อนการขึ้นบรรทัดใหม่ตัวสุดท้ายด้วย
กฎสคีมาของผู้ให้บริการเป็นส่วนหนึ่งของข้อกำหนด
กฎสคีมาของผู้ให้บริการโมเดลเป็นตัวตัดสินว่าคำขอจะถูกรับเลยหรือไม่ JSON object ที่ว่าง decode เป็น PHP array ที่ว่าง และ object ที่มีคีย์เป็นตัวเลข decode เป็น list เมื่อ encode กลับ สคีมาแบบนี้จะไปถึงผู้ให้บริการพร้อม array ในตำแหน่งที่ต้องเป็น object ผู้ให้บริการที่บังคับชนิดข้อมูล อย่างที่ function declaration ของ Gemini ทำ จะปฏิเสธทั้งคำขอ และทุกเทิร์นของบทสนทนาที่มีเครื่องมือนั้นจะล้มเหลว เราตรวจสอบสคีมาของ endpoint ระยะไกลด้วย walker ที่รู้ว่าค่าของแต่ละ keyword ใน JSON Schema เก็บอะไรไว้
9. ขีดจำกัดทรัพยากร
ทรัพยากรแต่ละอย่างต้องมีขีดจำกัด ณ จุดที่ถูกใช้ การตรวจขนาดหลังจาก body มาถึงแล้ว DNS lookup ที่ไม่มี deadline และ timeout ที่เป็นศูนย์ ต่างก็เปิดทางให้งานที่ไม่มีขีดจำกัด
| ทรัพยากร | รูปแบบความล้มเหลว | การควบคุม |
|---|---|---|
| ไบต์ของการตอบกลับ | การตรวจขนาดหลังจาก client คืนค่าแล้วปล่อยให้การรับส่งข้อมูลไม่มีขีดจำกัด: cURL handler ของ Guzzle เขียน body ทั้งหมดลงใน temporary stream ไปแล้ว | response sink ปฏิเสธการเขียนที่เกินเพดานตายตัวขณะที่ไบต์กำลังเข้ามา; ขีดจำกัดขนาดของ cURL เองยังคงอยู่เป็นขีดจำกัดชั้นที่สอง |
| เวลา | ใน Guzzle timeout เป็น 0 หมายถึงรอไม่มีกำหนด และเป็นค่าเริ่มต้น | timeout ของเครื่องมือต้องอยู่ระหว่าง 1 ถึง 120 วินาที และค่าที่ต่ำกว่าหนึ่งวินาทีจะถูกปฏิเสธตอนรัน |
| DNS | dns_get_record() ไม่มี timeout |
การ resolve ใน process ที่ถูกหยุดเมื่อถึง deadline ภายในงบของการเรียก |
| แค็ตตาล็อกเครื่องมือ | ทุกเครื่องมือที่เปิดใช้งานถูกอธิบายให้เอเจนต์ทุกตัวที่ให้บริการลูกค้าโดยตรงในทุกเทิร์น และผู้ให้บริการจำกัดจำนวน function declaration ต่อคำขอ | เครื่องมือ API ของเวิร์กสเปซไม่เกิน 32 ตัวในแค็ตตาล็อกของเทิร์นหนึ่ง และไม่เกิน 16,384 อักขระต่อสคีมา |
เพดานของแค็ตตาล็อกกำหนดขนาดโดยอิงขีดจำกัดที่ผู้ให้บริการเผยแพร่ซึ่งระมัดระวังที่สุด เอกสารอ้างอิง API ของ OpenAI ระบุค่าสูงสุดไว้ที่ 128 ฟังก์ชัน สำหรับเครื่องมือของ Chat Completions อย่างน้อยจนถึงเมษายน 2025 และเอกสารอ้างอิง function calling ของ Google ระบุ 128 function declaration ต่อคำขอ เมื่อมีเครื่องมือของเวิร์กสเปซ 32 ตัว แค็ตตาล็อกเต็มของเอเจนต์ StaffOS ที่ใหญ่ที่สุดยังคงต่ำกว่า 128
10. ความถูกต้องของผลลัพธ์
การควบคุมด้านการรักษาความลับไม่ได้ครอบคลุมความเสียหายทุกอย่างที่เครื่องมืออาจก่อ เมื่อ API คืนรายการระเบียนที่มีฟิลด์คู่ขนาน โมเดลอาจจับคู่ชื่อของระเบียนหนึ่งเข้ากับป้ายของอีกระเบียนหนึ่ง แล้วบอกสิ่งที่ไม่จริงกับลูกค้า การเรนเดอร์การตอบกลับของเราสามารถยุบแต่ละระเบียนให้เป็นข้อความบรรทัดเดียวก่อนที่โมเดลจะเห็น ฟิลด์ของระเบียนจึงไปถึงโมเดลในสภาพที่รวมกันอยู่แล้ว
เมื่อล้มเหลว การเรนเดอร์เป็นบรรทัดจะหันกลับไปหาข้อมูลต้นฉบับ ระเบียนที่ขาดฟิลด์ที่ถูกอ้างถึงจะถูกเก็บไว้ทั้งระเบียน และเทมเพลตที่ parse ไม่ได้จะไม่แตะ payload เลย การเรนเดอร์ปฏิเสธที่จะเขียน object ที่ path ที่ตั้งค่าไว้ใหม่ให้เป็น list และ path กับเทมเพลตจะถูกบันทึกพร้อมกัน หรือไม่ก็ไม่ถูกบันทึกเลย
11. วิธีที่เราสร้างและตรวจสอบการควบคุมด้านความปลอดภัย
การเปลี่ยนแปลงที่รอยต่อเหล่านี้ถูกสร้างและตรวจสอบดังนี้
- การรีวิวแบบ adversarial เอเจนต์รีวิวอัตโนมัติที่ได้รับคำสั่งให้ทำให้การเปลี่ยนแปลงพัง รีวิวแต่ละ revision และมักแนบวิธีทำซ้ำมาพร้อมกับข้อค้นพบแต่ละข้อ ผู้เขียนทำซ้ำข้ออ้างแต่ละข้อก่อนแก้โค้ด
- guard ที่ผ่านการตรวจแบบ mutation guard ใหม่แต่ละตัวจะถูกลบออกหรือปิดใช้งานในสำเนาของโค้ดที่ใช้แล้วทิ้ง และการเปลี่ยนแปลงจะนับก็ต่อเมื่อมี test ล้มเหลวหลังจากนั้น test ที่ผ่านได้โดยไม่มี guard ของตนจะถูกเขียนใหม่จนกว่าจะผ่านไม่ได้ วิธีนี้วางหลักไว้ในบทความปี 1978 ของ DeMillo, Lipton และ Sayward ว่าด้วยการเลือกข้อมูลทดสอบ
- ใช้ตารางข้อกำหนดแทนตัวอย่าง ข้อกำหนดแต่ละข้อคือตารางของกลุ่มอินพุต และแต่ละแถว assert ไบต์ที่ถูกส่งแบบตรงทุกไบต์ หรือ assert ว่าไม่มีสิ่งใดถูกส่ง:
- แอดเดรส special-purpose ใช้ตารางเดียวร่วมกันระหว่าง unit test ของนโยบายแอดเดรส การตรวจตอนบันทึก และการตรวจตอน dispatch
- รูปแบบของ URL รันผ่านการเรียกของโมเดลและ pre-fetch
- ตำแหน่งการวางใน body และ header รันผ่านการเรียกของโมเดลและทุกเส้นทางการทดสอบด้วยตนเอง
- header ที่คอนเน็กเตอร์ไม่มีวันส่ง ไขว้กับข้อความที่จะทำให้การเรียกถูกปฏิเสธหากถูกเรนเดอร์
- การกักกันข้อมูลรับรอง ไขว้เครื่องมือทั้งสองประเภทกับทุกตำแหน่งที่ข้อมูลรับรองอาจอยู่และความล้มเหลวทุกชนิด
- การบันทึก ครอบคลุมทุกเส้นทางที่บันทึกเครื่องมือ สถานะเริ่มต้น และรูปแบบอินพุตของเส้นทางนั้น
- หนึ่ง implementation ต่อหนึ่งกฎ กฎที่ตรวจทั้งตอนบันทึกและตอนรันคือฟังก์ชันเดียวที่ถูกเรียกสองครั้ง predicate เดียวกันที่ได้มาจากสองทางจะค่อย ๆ เบี่ยงออกจากกัน
- Fail closed กับสิ่งที่อ่านไม่ได้ โหมดการยืนยันตัวตนที่ไม่รู้จัก ข้อมูลรับรองที่ถอดรหัสไม่ได้ โฮสต์ที่ resolve ไม่ได้ JSON body ที่ซ้อนลึกเกินขีดจำกัดของ decoder และรูปแบบโค้ดที่ static guard parse ไม่ได้ ล้วนนำไปสู่การปฏิเสธ
12. การจับคู่กับ taxonomy สาธารณะ
การจับคู่นี้เราทำขึ้นเองเพื่อใช้เป็นแนวทาง การจับคู่นี้หลีกเลี่ยง CWE-200 และ CWE-269 ซึ่ง MITRE ไม่สนับสนุนให้ใช้ในการจับคู่ช่องโหว่
| ประเภทความล้มเหลว | CWE | OWASP Top 10 for LLM Applications 2025 | OWASP API Security Top 10 2023 |
|---|---|---|---|
| การควบคุมปลายทางขาออก | CWE-918 Server-Side Request Forgery; CWE-367 Time-of-check Time-of-use Race Condition, สำหรับ DNS rebinding | API7:2023 Server Side Request Forgery | |
| การสร้างคำขอ | CWE-180 Incorrect Behavior Order: Validate Before Canonicalize; CWE-116 Improper Encoding or Escaping of Output; CWE-235 Improper Handling of Extra Parameters | LLM05:2025 Improper Output Handling | |
| ขอบเขตของ secret | CWE-441 Unintended Proxy or Intermediary ("Confused Deputy"); CWE-863 Incorrect Authorization; CWE-532 Insertion of Sensitive Information into Log File | LLM02:2025 Sensitive Information Disclosure | |
| การกักกัน | CWE-209 Generation of Error Message Containing Sensitive Information; CWE-532 | LLM02:2025 | API10:2023 Unsafe Consumption of APIs |
| การกำหนดสิทธิ์ตามผลกระทบ | CWE-863; CWE-639 Authorization Bypass Through User-Controlled Key, สำหรับการมองเห็นระเบียน | API1:2023 Broken Object Level Authorization; API3:2023 Broken Object Property Level Authorization; API5:2023 Broken Function Level Authorization | |
| การเรียกอัตโนมัติ | CWE-863; CWE-367, สำหรับการอ่านการตั้งค่าซ้ำระหว่างเทิร์น | LLM06:2025 Excessive Agency | |
| ค่าจากโมเดลและแพลตฟอร์ม | CWE-639, สำหรับคีย์การค้นหาที่มิฉะนั้นโมเดลจะเลือกได้; CWE-441; CWE-116; CWE-625 Permissive Regular Expression | LLM01:2025 Prompt Injection; LLM05:2025 | API1:2023 Broken Object Level Authorization |
| ขีดจำกัดทรัพยากร | CWE-770 Allocation of Resources Without Limits or Throttling | LLM10:2025 Unbounded Consumption | API4:2023 Unrestricted Resource Consumption |
| ความถูกต้องของผลลัพธ์ | LLM09:2025 Misinformation |
สำหรับทีมที่สร้างระบบบน MCP security best practices ของ Model Context Protocol ครอบคลุมการโจมตีที่เกี่ยวข้องในบริบทของโปรโตคอลนั้นเอง รวมถึงการโจมตีแบบ confused deputy, token passthrough และ SSRF
ภาคผนวก: บันทึกการ implement
รายละเอียดสำหรับทีมที่สร้างการควบคุมแบบเดียวกัน
การรันทดสอบและการ normalize อินพุต
Laravel ตัดช่องว่างของอินพุตที่เป็นสตริงและแปลงสตริงว่างเป็น null ใน middleware ค่าเริ่มต้น route สำหรับการทดสอบด้วยตนเองได้รับการยกเว้นสำหรับค่าของอาร์กิวเมนต์ การรันทดสอบจึงเรนเดอร์ไบต์ตรงกันทุกไบต์กับที่การเรียกของโมเดลจะเรนเดอร์
อินพุตของฟอร์มที่ถูก flash
เมื่อฟอร์มการตั้งค่าไม่ผ่านการตรวจสอบ อินพุตที่ถูก flash กลับไปยัง session จะไม่รวมฟิลด์ข้อมูลรับรองของเครื่องมือทั้งสองประเภท ได้แก่ URL, header, การยืนยันตัวตน และ body ของเครื่องมือ HTTP API, ค่าของ secret ที่บันทึกไว้, access token และ URL, header และการยืนยันตัวตนของ endpoint ระยะไกลแต่ละตัวในรายการ ฟิลด์เหล่านี้ถูกลบออกหลังจากสิ่งใดก็ตามที่ flash อินพุต ดังนั้นรายการที่ใช้ชื่อเป็นคีย์ หรือ controller ที่ flash อินพุตเองก็ได้รับการครอบคลุมด้วย
Version token
แถวเครื่องมือที่เก็บข้อมูลรับรองมี version token สำหรับ optimistic locking digest ของแถวที่ไม่ใส่ salt จะทำให้ใครก็ตามที่ถือ token ทดสอบการเดาข้อมูลรับรองในแถวนั้นแบบออฟไลน์ได้ token จึงถูกคำนวณจากแถวร่วมกับ secret ของเซิร์ฟเวอร์ token ยังถูกคำนวณด้วย JSON_THROW_ON_ERROR: json_encode() คืนค่า false เมื่อเจอ UTF-8 ที่ไม่ถูกต้อง และหากไม่มี flag นี้ ทุกแถวแบบนั้นจะใช้ token เดียวกัน และผ่านการตรวจความล้าสมัยทุกครั้ง
วัดขนาดสคีมาแบบเดียว
ขีดจำกัดของสคีมาถูกวัดด้วยวิธีเดียวกันทั้งตอนบันทึกและตอนรัน การ encode JSON โดยไม่มี JSON_UNESCAPED_UNICODE เก็บอักขระที่ไม่ใช่ ASCII แต่ละตัวเป็น escape \uXXXX ยาวหกอักขระ การวัดข้อความที่เก็บไว้จึงจะนับสคีมาภาษาจีนเป็นราวหกเท่า และการวัดข้อความที่ส่งเข้ามาจะนับการเยื้องของฟอร์มการตั้งค่าไปด้วย ฟังก์ชันเดียวใช้กับการตรวจทั้งสอง: ฟังก์ชันนี้วัดสคีมาที่เขียนแบบกระชับ โดยนับอักขระแต่ละตัวเป็นตัวมันเอง
การแทนที่รายการ
การเขียนที่ส่งรายการ endpoint ระยะไกลมาจะแทนที่รายการที่เก็บไว้ทั้งรายการ array_replace_recursive() merge รายการตาม index การ merge จึงจะคงรายการย่อยที่เจ้าของลบออกไปแล้วไว้ พร้อมข้อมูลรับรองที่เก็บไว้ของรายการย่อยเหล่านั้น
ความยาวที่ประกาศไว้ของ 304
RFC 9110 §8.6 อนุญาตให้ 304 มี Content-Length ที่ 200 จะมี แม้ว่า 304 จะไม่มี body การตรวจขนาดการตอบกลับวัด body ที่ได้รับ และไม่มีวันอ่านความยาวที่ประกาศไว้
ผู้เขียนและการอ้างอิง
Vin Lim เป็นประธานเจ้าหน้าที่ฝ่ายเทคโนโลยี (CTO) ของ StaffOS ทำงานด้านการประสานการทำงานของเอเจนต์ การเชื่อมต่อระบบธุรกิจ และความปลอดภัยของเครื่องมือที่เอเจนต์ AI เรียกใช้
รูปแบบการอ้างอิงที่แนะนำ: Lim, V. (2026). Security engineering for AI agent connectors: nine failure classes. StaffOS technical paper, version 1.0, September 24. https://staffos.xyz/blog/ai-agent-connector-security
หากต้องการทราบว่าเราวางโครงสร้างและประเมินเอเจนต์ที่ใช้เครื่องมือเหล่านี้อย่างไร อ่านบทความของเราเรื่องวิศวกรรม Agent Harness
Frequently asked questions
ทำไมการเรียกเครื่องมือของเอเจนต์ AI จึงเป็นความเสี่ยงด้านความปลอดภัย? +
เอเจนต์ที่เรียก API ภายนอกอ่านข้อมูลส่วนตัวได้ อ่านเนื้อหาที่ไม่มีใครตรวจสอบ และส่งข้อมูลออกไปได้ Simon Willison เรียกการรวมกันนี้ว่า the lethal trifecta หากซอฟต์แวร์ที่ทำงานรอบโมเดลไม่ควบคุมปลายทาง เนื้อหาของคำขอ และการตอบกลับ พรอมป์ต์ที่ถูกบิดเบือนหรือเครื่องมือที่ตั้งค่าผิดก็ย้ายข้อมูลไปยังที่ที่ไม่ควรไปได้
จะป้องกัน SSRF อย่างไรเมื่อลูกค้าตั้งค่า URL ของ API เอง? +
รับเฉพาะ hostname ธรรมดาและ IP address ในรูปแบบ canonical ปฏิเสธทุกช่วงที่ทะเบียน special-purpose ของ IANA ระบุว่าไม่ globally reachable รวมถึงพื้นที่แอดเดรส loopback, private, shared และ link-local และแอดเดรสของ cloud metadata โดยใช้ตารางช่วงแอดเดรสที่ระบุชัดเจน resolve ชื่อภายใน deadline และ pin แอดเดรสที่ตรวจแล้วไว้กับการเชื่อมต่อ เพื่อไม่ให้คำตอบ DNS ครั้งที่สองเปลี่ยนแอดเดรสนั้นได้ ปิด redirect และปิดการตั้งค่า proxy ที่สืบทอดมาจาก environment
ทำไมการตรวจ URL ก่อนส่งจึงไม่เพียงพอ? +
URL ที่ guard ตรวจอาจต่างจาก URL ที่ถูกส่งออกไป HTTP client บางตัว expand ทุก URL เป็นเทมเพลต RFC 6570 หลังจากที่แอปพลิเคชันสร้าง URL นั้นแล้ว cURL เชื่อมต่อไปยังรูปแบบการเขียนโฮสต์ที่ parser แบบเข้มงวดไม่ถือว่าเป็นแอดเดรส และ proxy ก็ resolve ชื่อเอง การตรวจจะมีผลก็ต่อเมื่อตัดสินจากอินพุตเดียวกับที่ transport จะใช้
ควรเขียนอาร์กิวเมนต์จากโมเดลลงใน JSON body ของคำขออย่างไร? +
เขียนเป็น JSON ตามตำแหน่งที่อาร์กิวเมนต์แต่ละตัวอยู่ ภายใน JSON string ให้เขียนอาร์กิวเมนต์เป็นเนื้อหาของ string โดย escape เฉพาะจุดที่ JSON กำหนด ที่ตำแหน่งค่า ให้เขียนค่า JSON ที่โมเดลส่งมา การ percent-encode ทำให้ upstream อ่านข้อความที่ถูก encode ตามตัวอักษร และการวางข้อความดิบลงไปเปิดทางให้เครื่องหมายอัญประกาศเพิ่มคีย์ได้ ปฏิเสธ body ที่ไม่เป็น JSON ที่ถูกต้องเมื่อเขียนทุกค่าลงไปแล้ว
อาร์กิวเมนต์จากโมเดลเปลี่ยน endpoint ของ API ที่เครื่องมือเรียกได้หรือไม่? +
ไม่ควรเปลี่ยนได้ ให้ percent-encode ทุกอาร์กิวเมนต์ที่เขียนลงใน URL ปฏิเสธไวยากรณ์เทมเพลตใดก็ตามที่หลงเหลืออยู่ และปฏิเสธค่าที่ก่อให้เกิด path segment . หรือ .. libcurl ลบ dot segment ก่อนส่งคำขอ ดังนั้นเทมเพลตอย่าง /customers/{id}/orders ที่มี id เป็น .. จะไปถึง /orders บนโฮสต์ของ operator พร้อมข้อมูลรับรองของเครื่องมือ
แพลตฟอร์มเอเจนต์ AI ควรกำหนดขอบเขตของ secret สำหรับ API อย่างไร? +
เก็บ secret ที่บันทึกไว้ของแต่ละเวิร์กสเปซในที่เก็บที่เข้ารหัสของเวิร์กสเปซนั้นเอง และ resolve การอ้างอิงจากที่เก็บนั้นเท่านั้น จากนั้นกำหนดขอบเขตของ secret ที่บันทึกไว้ตามการใช้งาน: เฉพาะคนที่มีสิทธิ์จัดการ secret เท่านั้นที่ควรเปลี่ยนปลายทางของคำขอที่นำ secret นั้นไปด้วยได้ ทดสอบคำขอนั้นได้ หรือเปิดการเรียกอัตโนมัติที่ส่ง secret นั้นได้
จะกันข้อมูลรับรองออกจากสิ่งที่โมเดลเห็นได้อย่างไร? +
แทนที่ข้อความข้อผิดพลาดของ transport ด้วยการจัดประเภทและประโยคตายตัว เพราะข้อผิดพลาดของไลบรารี HTTP อาจยก URL, hostname และค่าของ header มาแสดง สำหรับการตอบกลับของเครื่องมือ ให้ปกปิดข้อมูลรับรองที่เครื่องมือส่งออกไปในทุกรูปแบบการเขียนที่ JSON อนุญาต และระงับ JSON body ที่ซ้อนลึกเกินกว่าจะ decode ได้
โมเดลเลือกได้หรือไม่ว่าเครื่องมือจะค้นหาลูกค้ารายใด? +
ทำได้ผ่านอาร์กิวเมนต์ที่สคีมาของเครื่องมือประกาศไว้เท่านั้น แพลตฟอร์มเติมเบอร์โทรศัพท์ของบทสนทนาและรายละเอียดของผู้ติดต่อให้ และสคีมาที่ประกาศอาร์กิวเมนต์ที่ใช้ชื่อใดชื่อหนึ่งในนั้นจะถูกปฏิเสธตอนบันทึกเครื่องมือ และถูกปฏิเสธอีกครั้งตอนสร้างเครื่องมือสำหรับเทิร์น secret ที่บันทึกไว้ไม่มีวันถูกอ่านจากอาร์กิวเมนต์เลย
References
- [1] Willison, S. The lethal trifecta for AI agents: private data, untrusted content, and external communication. 16 June 2025.
- [2] OWASP Cheat Sheet Series. Server-Side Request Forgery Prevention Cheat Sheet.
- [3] IANA. IPv4 Special-Purpose Address Registry.
- [4] IANA. IPv6 Special-Purpose Address Registry.
- [5] Alibaba Cloud documentation. View instance metadata (metadata service at 100.100.100.200).
- [6] OWASP. Top 10 for LLM Applications 2025.
- [7] OWASP. API Security Top 10 2023.
- [8] Model Context Protocol. Security Best Practices.
- [9] IETF RFC 6570. URI Template. Sections 3.2.1, 3.2.3 and 3.2.4.
- [10] IETF RFC 3986. Uniform Resource Identifier: Generic Syntax. Section 5.2.4, Remove Dot Segments.
- [11] IETF RFC 8259. The JSON Data Interchange Format. Section 7.
- [12] IETF RFC 9110. HTTP Semantics. Section 8.6, Content-Length.
- [13] Laravel framework source. PendingRequest::send() expands every URL through UriTemplate::expand().
- [14] Laravel documentation. HTTP Client, URI Templates.
- [15] Laravel documentation. Requests, Input Trimming and Normalization.
- [16] Laravel documentation. Validation, Implicit Rules.
- [17] Guzzle uri-template source. UriTemplate::expand().
- [18] Guzzle source. Client reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY from the environment.
- [19] Guzzle documentation. Request options: allow_redirects and timeout.
- [20] Requests documentation. Proxies.
- [21] Requests documentation. Redirection and History.
- [22] Go standard library. net/http: DefaultTransport, ProxyFromEnvironment and Client.CheckRedirect.
- [23] Node.js documentation. NODE_USE_ENV_PROXY.
- [24] MDN Web Docs. RequestInit: redirect.
- [25] curl documentation. CURLOPT_RESOLVE.
- [26] curl documentation. CURLOPT_PATH_AS_IS.
- [27] curl documentation. CURLOPT_PROXY.
- [28] PHPWord source. Word2007 reader loads an external VML image from the URL the document names.
- [29] PHP manual. parse_str.
- [30] PHP manual. strtr.
- [31] PHP manual. PCRE anchors.
- [32] Meta for Developers. Onboarding business customers as a Tech Provider (Embedded Signup token exchange).
- [33] OpenAI openai-openapi specification, commit 498c71d. Chat Completions tools: "A max of 128 functions are supported."
- [34] Google Cloud. Function calling reference, limitations.
- [35] Hardy, N. The Confused Deputy (or why capabilities might have been invented). ACM SIGOPS Operating Systems Review 22(4), 1988.
- [36] DeMillo, R. A., Lipton, R. J., Sayward, F. G. Hints on Test Data Selection: Help for the Practicing Programmer. IEEE Computer 11(4), 1978.
- [37] MITRE. CWE-918: Server-Side Request Forgery (SSRF).
- [38] MITRE. CWE-639: Authorization Bypass Through User-Controlled Key.
About the author
Vin Lim
ผู้ร่วมก่อตั้ง / CTO, StaffOS
Vin Lim เป็น CTO ของ StaffOS งานของเขาครอบคลุมการประสานการทำงานของเอเจนต์ การเชื่อมต่อระบบธุรกิจ และการควบคุมที่กำหนดว่าเอเจนต์ AI เข้าถึงอะไร ส่งอะไร และเปิดเผยอะไรได้
Related reading
วิศวกรรม Agent Harness: เพิ่มประสิทธิภาพ AI โดยไม่ทำ fine-tuning
StaffOS ออกแบบบริบท เครื่องมือ และการควบคุมการทำงานอย่างไรโดยคงน้ำหนักโมเดลเดิม พร้อมผลวิจัยสาธารณะ กรณีทดสอบสังเคราะห์ และกราฟที่สร้างซ้ำได้
Speed-to-lead ในยุคของเอเจนต์: เส้นโค้งจาก HBR ยังจริง แต่ตอนนี้พื้นวัดกันที่หลักวินาที
เส้นโค้ง speed-to-lead จาก HBR ปี 2011 ยังจริงอยู่ แต่ค่ามัธยฐานการตอบลีดในอุตสาหกรรมกลับแย่ลง นี่คือสิ่งที่ AI SDR เปลี่ยน และเหตุผลที่พื้นใหม่วัดกันที่หลักวินาที
เอเจนต์ AI ทำให้ drip cadence ล้าสมัยไปแล้ว การหล่อเลี้ยงตามสัญญาณคือสิ่งที่มาแทน
drip cadence ถูกสร้างขึ้นในยุคที่การปรับให้เป็นส่วนตัวมีต้นทุนสูงและการส่งข้อความแทบไม่มีต้นทุน LLM พลิกเศรษฐศาสตร์นั้นกลับด้าน นี่คือเหตุผลสำหรับการหล่อเลี้ยงตามสัญญาณในยุคของเอเจนต์