ข้ามไปยังเนื้อหา
Pre-MVP โค้ดเบส 1.0 ยังไม่ใช่ผลิตภัณฑ์ที่เผยแพร่ — ดู ขอบเขตผลิตภัณฑ์ 1.0

ความปลอดภัยของปลั๊กอิน

installation token ของ Dee Wan คืออะไร ถูกเก็บและ rotate อย่างไร สิทธิ์อำนาจของบุคคลหนึ่งไปถึง คำสั่งของปลั๊กอินได้อย่างไร grant จำกัดขอบเขตอะไรบ้างจริง ๆ และผู้ที่ยึดปลั๊กอินได้สามารถทำอะไรได้ และทำอะไรไม่ได้

ทุกตัวอย่างในหน้านี้ถูก execute กับ route จริงโดย backend/test/plugin-docs.test.ts id และ secret ทั้งหมดเป็นข้อมูลสังเคราะห์

ดูเพิ่มเติมที่ Protocol v1 สำหรับพื้นผิวทีละ route และ การเขียนปลั๊กอิน สำหรับขั้นตอนการติดตั้ง

browser ── user session ──► Dee Wan admin Worker
│ control database: installations, token hashes, grants, launch codes
│ tenant database: content, versions, workflow, audit
│ HTTPS + installation bearer token
plugin service ◄──────────────┘
│ its own database, its own clock, its own sessions
└── HTTPS + installation bearer token ──► /api/plugin/v1 ──► the same lifecycle service a person uses

คุณสมบัติสามข้อค้ำขอบเขตนั้นไว้:

  1. Plugin API ไม่ใช่ admin API มันอยู่ภายใต้ /api/plugin/v1 มี middleware ของตัวเอง และใช้ร่วมกันเพียง lifecycle service — ไม่มี implementation ที่สองของ transition ที่ อาจคลาดเคลื่อนไปจากตัวที่บุคคลใช้งาน
  2. ไม่มีโค้ดของปลั๊กอิน ไม่มี markup ของปลั๊กอิน core ไม่โหลด module ใด ไม่ประมวลผล script ใด ไม่ render HTML หรือ CSS จากระยะไกล และไม่ฝัง iframe อิทธิพลเดียวที่ปลั๊กอินมีต่อ admin UI คือ action { id, label } ที่มีขอบเขตจำกัด และลิงก์หนึ่งลิงก์
  3. ปลั๊กอินไม่มีสิทธิ์เข้าถึงฐานข้อมูล ปลั๊กอินไม่ได้รับ Prisma client ไม่ได้รับ D1 binding ไม่ได้รับ credential ของ R2 และไม่ได้รับสิทธิ์เข้าถึงบัญชี Cloudflare ทุกสิ่งที่มันอ่านหรือเปลี่ยนได้ต้องผ่านห้า route
รูปแบบ dwp_ ตามด้วยอักขระแบบ URL-safe 43 ตัว — ความสุ่ม 256 bit จาก crypto.getRandomValues
ให้แก่ปลั๊กอิน ครั้งเดียว ในการส่งมอบตอน activation ผ่าน HTTPS ไปยัง origin ของ manifest เอง
core เก็บ เฉพาะ hash แบบ SHA-256 ไม่มีเส้นทางโค้ดใดที่พิมพ์ token ออกมาได้อีก
ปลั๊กอินส่ง Authorization: Bearer <token> ในทุก request
ไม่มีวันอยู่ใน URL, query parameter, error body, บรรทัด log, audit entry หรือ fixture
ขอบเขต installation (การติดตั้ง Dee Wan หนึ่งชุด) หนึ่งรายการ ซึ่งคือปลั๊กอินหนึ่งตัวบนไซต์หนึ่งไซต์

prefix มีไว้เพื่อให้สตริงที่รั่วไหลถูกจดจำได้ว่าเป็น credential ของปลั๊กอิน Dee Wan ส่วน entropy คือสิ่งที่ทำให้มันเป็น credential ค่า bearer ที่ผิดรูปแบบจะถูกปฏิเสธจากรูปแบบก่อนการอ่านฐานข้อมูลใด ๆ ดังนั้นความพยายามเดาจะทำให้ผู้โจมตีเสีย lookup ที่ไปไม่ถึงเลย

rotation คือการส่งมอบ ไม่ใช่การ reset และต้องใช้โค้ดแบบใช้ครั้งเดียวใหม่จากปลั๊กอิน — มิฉะนั้น ใครก็ตามที่เข้าถึง admin route ได้ก็สามารถแทนที่ credential ได้อย่างเงียบ ๆ

  1. ผู้ดูแลได้รับ activation code ใหม่จากปลั๊กอิน
  2. core ออก token ใหม่และส่งมันพร้อมโค้ดนั้นไปยัง activation_url ที่ถูก pin ไว้จาก manifest ที่เก็บไว้ ไม่ใช่ไปยัง URL ที่ส่งมาตอน rotation
  3. core จะสลับ hash เฉพาะเมื่อได้ 2xx เท่านั้น หากปลั๊กอินปฏิเสธหรือติดต่อไม่ได้ token เก่า ยังคงใช้งานได้ — rotation ที่ล้มเหลวต้องไม่มีวันล็อก installation ที่ใช้งานได้อยู่ออกไป
  4. hash ก่อนหน้ายังใช้ได้นานสูงสุด 10 นาที เพื่อไม่ให้ request ที่กำลังส่งอยู่ ถูกตัดกลางคันระหว่าง rotation
  5. request แรกที่ถือ token ใหม่ (NEW) จะยุติช่วงซ้อนทับนั้นทันที ปลั๊กอินที่รับ token ใหม่มาใช้อย่างรวดเร็วจะลดช่วงเวลาที่ตัวเองเสี่ยงให้แคบลง

activation code ไม่มีวันถูกเก็บและไม่มีวันถูกบันทึกลง log ไม่ว่าฝั่งใด

ทั้ง disabled และ revoked หยุด plugin API ตั้งแต่หน้าประตู — ก่อนการเข้าถึง tenant ก่อนที่ body จะถูกอ่าน revoked เป็นสถานะสุดท้าย: hash ถูกทิ้ง และ token นั้นไม่มีวันใช้ได้อีก การ uninstall จะ revoke ก่อน และ Dee Wan ไม่อ้างสิทธิ์ใด ๆ เกี่ยวกับข้อมูลที่มันมองไม่เห็น

UI สำหรับจัดการของปลั๊กอินเป็นหน้าแยกต่างหาก และ session cookie ของ Dee Wan ไม่มีวันออกนอก Dee Wan ตัวเชื่อมระหว่างทั้งสองคือ launch code:

  • สุ่ม ใช้ครั้งเดียว เก็บแบบ hash และใช้ได้ 60 วินาที
  • ผูกกับ installation, ไซต์, ผู้ใช้, action และ — สำหรับ action บนเนื้อหา — instance ของเนื้อหาหนึ่งรายการ
  • ถูกแลกเปลี่ยนโดยแบ็กเอนด์ (BACKEND) ของปลั๊กอิน ด้วย bearer token ของตัวเอง เพื่อรับ delegation record
  • โค้ดที่ออกให้ installation หนึ่งไม่สามารถถูกแลกเปลี่ยนโดยอีก installation หนึ่งได้
  • launch response มี Referrer-Policy: no-referrer และหน้าระยะไกลต้องไม่โหลด resource จากบุคคลที่สามใด ๆ ก่อนการแลกเปลี่ยน

core ตรวจสอบก่อนออกโค้ด: ผู้ร้องขอมี session และสิทธิ์เข้าถึงไซต์ manage ต้องการ site:plugins และ action บนเนื้อหาต้องการสิทธิ์อ่านรายการนั้นโดยเฉพาะ

delegation ไม่ใช่ credential มันไม่มี capability ใด ๆ สิ่งที่มันมีคือตัวตน — และ นั่นคือสิ่งที่ทำให้การอ่านสิทธิ์อำนาจใหม่เป็นไปได้:

คำสั่งของปลั๊กอินทุกคำสั่งระบุ delegation_id และ core จะอ่านสิทธิ์อำนาจปัจจุบัน (CURRENT) ของบุคคลนั้นใหม่ จาก control database ก่อนจะเขียนอะไรก็ตาม สิทธิ์อำนาจที่บันทึกไว้ตอนสร้างตารางเวลา จะไม่มีวันถูกส่งต่อไปใช้

ดังนั้นกรณีเหล่านี้ทั้งหมดจะถูกปฏิเสธตอน execute แม้ว่า delegation จะใช้ได้ตอนที่ถูกสร้าง: บุคคลนั้น เสียบทบาท (role) บนไซต์ บัญชีของเขาถูกปิดใช้งาน บทบาทของเขาไม่มีสิทธิ์ เผยแพร่อีกต่อไป หรือ delegation เป็นของอีก installation หนึ่ง การอ่านที่มี delegation_id จะจำกัด transition ที่เสนอให้แคบลงในลักษณะเดียวกัน และปฏิเสธแทนที่จะคืน รายการที่สั้นลงอย่างเงียบ ๆ

บุคคลที่ร้องขอไม่มีสิทธิ์อำนาจที่จำเป็นอีกต่อไป

Terminal window
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0009:r1" \
--data '{"kind":"publish","expected_current_version_id":"cdocsversion00000000000001","expected_workflow_revision":0,"delegation_id":"pld_0000000000000000000000000000d1ee"}'
{
"outcome": {
"code": "requester_unauthorized",
"message": "The person who requested this no longer holds the authority it needs.",
"retry": false
}
}

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

ข้อยกเว้นเดียวที่ตั้งใจไว้คือ replay Idempotency-Key ที่ถูกนำไปใช้แล้วจะคืน ผลลัพธ์แรกแม้ผู้ร้องขอจะเสียสิทธิ์อำนาจไปแล้วก็ตาม — งานเสร็จไปแล้ว และ การตอบซ้ำไม่ใช่การมอบสิทธิ์ใหม่

ไซต์มาจาก token และไม่มาจากสิ่งใดที่ผู้เรียกส่งมา ไม่มี X-Site-Id ใน request ของปลั๊กอิน ไม่มีไซต์ใน body และไม่มีชื่อฐานข้อมูลที่ใดเลย: ทั้งสองอย่างถูกปฏิเสธทันที แทนที่จะ ถูกเพิกเฉย ดังนั้น bug ในปลั๊กอินจึงไม่สามารถกลายเป็นการอ่านข้าม tenant ได้

การระบุชื่อฐานข้อมูล

Terminal window
curl "https://cms.example/api/plugin/v1/context" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "X-D1-Binding: SOME_OTHER_TENANT"
{
"outcome": {
"code": "invalid_request",
"message": "A plugin request cannot name a database.",
"retry": false
}
}

สิทธิ์เข้าถึงโมเดลคือส่วนร่วม (intersection) ของ capability และรายการ id ของโมเดลเนื้อหาที่เก็บไว้ สิ่งที่อยู่นอก ขอบเขตจะตอบ content_not_found — คำตอบเดียวกับ “ไม่มีเนื้อหานี้” ดังนั้นปลั๊กอินจึงไม่สามารถทำแผนที่ โครงสร้างของไซต์ด้วยการสุ่มตรวจได้

id จากไซต์อื่น

Terminal window
curl "https://cms.example/api/plugin/v1/content/cotherinstance0000000000001" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "content_not_found",
"message": "No such content in this installation’s scope.",
"retry": false
}
}

grant ที่ถูกจำกัดให้แคบลงมีผลกับ request ถัดไปทันที:

หลังจากผู้ดูแลลบโมเดลออกจาก grant

Terminal window
curl "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "content_not_found",
"message": "No such content in this installation’s scope.",
"retry": false
}
}

grant เก็บ id ของโมเดล ดังนั้นการเปลี่ยนชื่อโมเดลจะคง grant ไว้ และการลบโมเดลจะทำให้ส่วนนั้น ของ grant ไม่มีผล UI ด้านการดูแลระบบแสดง slug ปัจจุบันไว้ข้าง id

cookie ของผู้ใช้ไม่ใช่การยืนยันตัวตนของปลั๊กอิน route ของปลั๊กอินอ่าน Authorization และไม่อ่านอย่างอื่น ดังนั้น browser session ที่ถูกขโมยไปจึงไม่สามารถนำมา replay กับ plugin API ได้:

session cookie บน route ของปลั๊กอิน

Terminal window
curl "https://cms.example/api/plugin/v1/context" \
-b "__Secure-better-auth.session_token=$SESSION"
{
"outcome": {
"code": "plugin_unauthorized",
"message": "The installation token is missing, unknown, disabled or revoked.",
"retry": false
}
}

ทิศทางกลับกันก็เป็นจริงเช่นกัน และครอบคลุมโดย backend/test/plugin-installation.test.ts: token ของปลั๊กอิน ที่ถูกนำไปใช้กับ route ด้านการดูแลระบบจะถูกปฏิเสธในฐานะที่ไม่ได้ยืนยันตัวตน route เหล่านั้นต้องการ session ของผู้ใช้ header ของไซต์ และ site:plugins ปลั๊กอินไม่มีทั้งสามอย่าง

installation ที่ถูกปิดใช้งาน ก่อนการเข้าถึง tenant ใด ๆ

Terminal window
curl "https://cms.example/api/plugin/v1/context" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "plugin_unauthorized",
"message": "The installation token is missing, unknown, disabled or revoked.",
"retry": false
}
}

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

request ถูกจำกัดต่อ installation — 120 ครั้งต่อช่วง 60 วินาที — โดยใช้กลไกบังคับใช้ พื้นฐานเดียวกับส่วนอื่นของผลิตภัณฑ์ และตอบกลับเป็น envelope ของโปรโตคอลเองพร้อม Retry-After สิ่งเหล่านี้คือการควบคุมการใช้งานในทางที่ผิด ไม่มีสิ่งใดในนี้ถูกวัด รายงาน หรือเก็บไว้เป็น telemetry

เกินขีดจำกัด

Terminal window
curl "https://cms.example/api/plugin/v1/context" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "rate_limited",
"message": "Too many requests for this installation.",
"retry": true
}
}

เคารพ Retry-After และปฏิเสธ record ของคุณเองแทนการรอ หากระยะหน่วงเกินค่าสูงสุด ที่คุณเลือกไว้ล่วงหน้า

core เรียกปลั๊กอินเพียงสองครั้งเท่านั้น: การดึง manifest และการส่งมอบตอน activation หรือ rotation ทั้งสอง มีขอบเขตจำกัดและถูก pin ไว้

  • HTTPS เท่านั้น credential ใน URL, fragment, loopback, link-local, ปลายทาง private และ reserved ถูกปฏิเสธทั้งหมด รวมถึง IP literal และชื่อที่ไม่มีจุด
  • redirect ถูกปฏิเสธ ไม่ถูกติดตาม 3xx คือ remote_redirect_refused
  • response body ถูกจำกัดที่ 32 KB และการเรียกที่ 5 วินาที
  • ทุก URL — manifest, base, activation, management — ต้องใช้ origin เดียวกัน ดังนั้น activation จึง ไม่สามารถถูกชี้ไปยังที่ที่ manifest ที่ผ่านการตรวจทานไม่ได้ระบุไว้
  • response จากระยะไกลถูก parse เป็น input ที่ไม่น่าเชื่อถือเทียบกับ manifest schema key ที่ไม่รู้จักคือ การปฏิเสธ ไม่ใช่สิ่งที่จะเพิกเฉย
  • Workers ไม่สามารถ resolve DNS ก่อน fetch ได้ ดังนั้นสิ่งที่ตรวจคือสิ่งที่ URL ระบุเอง ชื่อสาธารณะ ที่ resolve ไปเป็นที่อยู่ private อยู่นอกสิ่งที่การตรวจมองเห็น และมีระบุไว้เช่นนั้นใน url-policy.ts
  • HTTP บนเครื่อง local เป็นไปได้ เฉพาะ เมื่อ ENVIRONMENT=development และ origin ที่ตรงตัวอยู่ในรายการ PLUGIN_DEV_ORIGINS ไม่มี wildcard ไม่มีทางลัดสำหรับช่วง private ไม่มี fallback ใน production

ปลั๊กอินเป็นผู้กระทำ (actor) ไม่ใช่ผู้ใช้ ไม่มีการสร้างผู้ใช้ปลอม และไม่มีการเขียน installation id ลงใน foreign key ของผู้ใช้

transition ของปลั๊กอินบันทึก installation id, plugin id ที่ได้รับการยอมรับ ชื่อและเวอร์ชัน, เวอร์ชันของ โปรโตคอล, digest ของ idempotency key — ไม่ใช่ตัว key เอง — สถานะผลลัพธ์และ publication pointer และอีเมลของบุคคลที่ delegation ของเขาอนุญาตการกระทำนั้น ตัวตนของ installation อยู่ใน control database และ workflow audit อยู่ใน tenant database โดยไม่มี foreign key ข้ามฐานข้อมูล: event ของ tenant เก็บ snapshot ที่เปลี่ยนแปลงไม่ได้ นั่นคือเหตุผลเช่นกันว่าทำไม การ uninstall จึงไม่สามารถลบการระบุผู้กระทำได้

การกระทำด้านการดูแลระบบ — install, การเปลี่ยน grant, การยอมรับ manifest, rotation, disable, enable, uninstall — ถูกเขียนลงใน admin audit ledger โดยผู้ใช้ที่กระทำ และจะปฏิเสธ (503 audit_unavailable) แทนที่จะเปลี่ยนแปลงโดยไม่มี audit

หากปลั๊กอินถูกยึดครองทั้งหมด ผู้โจมตีจะถือ installation token หนึ่งตัวสำหรับไซต์หนึ่งไซต์ และสามารถ:

  • อ่านสรุปเนื้อหาแบบแคบของโมเดลที่ได้รับ grant — id, label, slug, version id, สถานะ workflow และ transition ที่ขอบเขต
  • เผยแพร่หรือยกเลิกการเผยแพร่เนื้อหาในโมเดลเหล่านั้น แต่ต้องระบุ delegation_id ที่ยังใช้ได้ ซึ่งบุคคลนั้นยังมีสิทธิ์อำนาจอยู่ pin เวอร์ชันที่ตรงตัว และตรงกับ workflow revision ปัจจุบันเท่านั้น
  • เปิด route แลกเปลี่ยนด้วยโค้ดที่มันถืออยู่แล้ว

มันไม่สามารถ:

  • เข้าถึงไซต์อื่น โมเดลอื่น หรือ delegation ของ installation อื่น
  • อ่านหรือเขียนค่าฟิลด์ media ผู้ใช้ บทบาท session การตั้งค่า โมเดล หรือ audit
  • รันโค้ด render markup หรือเข้าถึงฐานข้อมูล R2 หรือ credential ใด ๆ ของ Cloudflare
  • เผยแพร่ draft ที่ใหม่กว่าตัวที่ถูก pin ข้าม workflow ที่เปลี่ยนไปแล้ว override เนื้อหาที่ ขึ้นต่อกัน หรือใช้ override route ใด ๆ — ไม่มีเลยใน protocol v1
  • กระทำแทนบุคคลที่เสียสิทธิ์อำนาจไปแล้ว หรือปลอมแปลงบุคคล: delegation_id คือการค้นหา ไม่ใช่ การยืนยัน
  • replay การเปลี่ยนแปลงให้เกิดผลครั้งที่สอง หรือซ่อนการเปลี่ยนแปลงจาก audit trail

หาก launch code รั่วไหล — ผ่าน referrer, log หรือ URL ที่แชร์ — มันใช้ได้ครั้งเดียว อายุไม่เกิน 60 วินาที เก็บแบบ hash และไร้ประโยชน์หากไม่มี installation token ที่ใช้แลกเปลี่ยนมัน

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

สิ่งที่ Dee Wan ไม่สามารถป้องกันได้ คือการดำเนินงานของปลั๊กอินเอง: uptime นาฬิกา นโยบาย retry สิ่งที่มันเก็บในฐานข้อมูล และมันลบข้อมูลของคุณหรือไม่เมื่อคุณขอ เอกสารของปลั๊กอินเองต้องตอบคำถามเหล่านั้น และผู้ดูแลต้องอ่านมันก่อนจะมอบ grant ใด ๆ ดู ปลั๊กอิน Scheduling สำหรับตัวอย่างว่า สิ่งนั้นมีหน้าตาอย่างไร