Security Model
โดย default โค้ด Node รันด้วยสิทธิ์เต็มของ OS user ที่เริ่มมัน — ไม่มี sandbox, ไม่มี capability system เว้นแต่คุณจะ opt-in เข้าไปเอง บทนี้ครอบคลุมขอบเขตที่ Node ให้คุณจริง ๆ และ — สำคัญพอกัน — จุดที่ดูเหมือนเป็นขอบเขตแต่จริง ๆ ไม่ใช่
--permission: allowlist ที่ชัดเจน แบบ opt-in
หัวข้อที่มีชื่อว่า “--permission: allowlist ที่ชัดเจน แบบ opt-in”ด้วย --permission Node ปฏิเสธการเข้าถึง filesystem, network, child-process, และ worker โดย default และคุณให้สิทธิ์คืนเฉพาะที่ process ต้องการ:
node read-secret.jstop secretnode --permission read-secret.jsError: Access to this API has been restricted. Use --allow-fs-read to manage permissions. code: 'ERR_ACCESS_DENIED', permission: 'FileSystemRead', resource: './secret.txt'node --permission --allow-fs-read=./secret.txt read-secret.jstop secretนี่คือขอบเขตความปลอดภัยจริง (ต่างจากสอง “ขอบเขต” ด้านล่าง): permission model ถูกบังคับใช้โดย Node เองก่อน operation จะรัน ไม่ใช่แค่ตามธรรมเนียม มันยังถูกติดป้าย experimental อยู่ — ปฏิบัติกับมันเป็น defense-in-depth สำหรับ process ที่รันโค้ดที่เชื่อถือได้น้อยกว่า ไม่ใช่สิ่งเดียวที่กั้นระหว่าง process ของคุณกับ filesystem ยัง
--env-file: โหลด secret โดยไม่ต้องมี dependency
หัวข้อที่มีชื่อว่า “--env-file: โหลด secret โดยไม่ต้องมี dependency”Node อ่านไฟล์สไตล์ .env ได้ตรง ๆ ไม่ต้องมี package dotenv:
Output (verified on Node 24)
API_KEY without --env-file: undefinednode --env-file=.env read-env.jsAPI_KEY is super-secret-valueนิสัยที่เกี่ยวกับความปลอดภัยที่สิ่งนี้แทนที่: การพิมพ์ process.env (หรือ object ที่รวมมันไว้) ใน error handler หรือ debug log เป็นวิธีที่ secret รั่วเข้าไปใน log aggregator ที่คนจำนวนมากอ่านได้ — ปฏิบัติกับ process.env เองว่าเป็นข้อมูล sensitive ไม่ใช่แค่ไฟล์ .env ที่มันมาจาก
node:crypto ใช้อย่างถูกต้อง
หัวข้อที่มีชื่อว่า “node:crypto ใช้อย่างถูกต้อง”ความผิดพลาดที่พบบ่อยสามอย่างเจาะจงและวิธีแก้ ทั้งหมดเป็น behavior จริงบน Node 24.13:
Output (verified on Node 24)
tokens must come from a CSPRNG, not Math.random
random token length (hex chars): 32
same password + same salt -> same hash: true
timingSafeEqual result: trueใช้ scrypt/pbkdf2 (ไม่ใช่ sha256/sha512 ตรง ๆ) สำหรับ password เพราะ hash ทั่วไปที่เร็วเปิดโอกาสให้ผู้โจมตีที่มี hash database ที่หลุดออกไปลองเดาเป็นพันล้านครั้งต่อวินาทีบนฮาร์ดแวร์ทั่วไป — ต้นทุน memory ที่ scrypt จงใจใส่ไว้ทำให้แพงขึ้นมาก timingSafeEqual สำคัญทุกที่ที่คุณเทียบ secret กับ input ที่ผู้ใช้ส่งมา (API key, HMAC signature): === แบบไร้เดียงสา short-circuit ที่ byte แรกที่ไม่ตรงกัน และความต่างของเวลาเล็กน้อยระหว่าง “พังที่ byte 1” กับ “พังที่ byte 30” คือ side channel ที่ใช้โจมตีได้จริงถ้ายิง request มากพอ
Prototype pollution: deep-merge แบบไร้เดียงสาคือช่องโจมตี
หัวข้อที่มีชื่อว่า “Prototype pollution: deep-merge แบบไร้เดียงสาคือช่องโจมตี”Recursive merge ที่ไม่เช็ก key __proto__/constructor/prototype เปิดให้ JSON ที่ผู้โจมตีควบคุมไปถึง Object.prototype เอง:
Output (verified on Node 24)
innocentObject.isAdmin (should not exist): trueการ merge ไม่เคยแตะ innocentObject ตรง ๆ เลย — มันปนเปื้อน Object.prototype ดังนั้น plain object ทุกตัว ใน process ตอนนี้ inherit isAdmin: true รวมถึงตัวที่ถูกสร้างก่อนหรือหลังการ merge ที่เป็นอันตรายนั้นนานแล้วก็ตาม นี่ไม่ใช่ตัวอย่างที่สร้างขึ้นมาลอย ๆ — มันคือ vulnerability class จริงที่ถูกเปิดเผยซ้ำแล้วซ้ำเล่าใน npm package ที่ implement deep-merge/clone/extend ของตัวเอง วิธีแก้: ปฏิเสธ key __proto__/constructor/prototype อย่างชัดเจนก่อน recurse, ใช้ Object.create(null) สำหรับ object ที่ตั้งใจให้ถือ key จากผู้ใช้แบบใดก็ได้, หรือใช้ merge utility จาก library ที่แพตช์เรื่องนี้ไว้แล้ว (แล้วอัปเดตมันไว้เสมอ — บั๊กประเภทนี้เป๊ะ ๆ ถูกนำกลับมาโดย library ที่ “แก้แล้ว” มากกว่าหนึ่งครั้ง) โมดูล practical-mastery ของคอร์ส TypeScript เดินผ่าน vulnerability เดียวกันนี้จากมุม type-safety ถ้าคุณอยากได้อีกครึ่งหนึ่งของภาพ
eval, new Function, และ vm ไม่ใช่ sandbox
หัวข้อที่มีชื่อว่า “eval, new Function, และ vm ไม่ใช่ sandbox”vm.createContext ดูเหมือนจะแยกโค้ดออกจากกัน — มันแยก global ไม่ใช่ object model ที่อยู่ข้างใต้ constructor.constructor เข้าถึง Function constructor ตัวจริงจากข้างใน context ที่ “sandboxed” ได้ และ constructor นั้นไม่ได้ถูก scope ให้อยู่ใน vm เลย:
Output (verified on Node 24)
escaped the vm context, read a value from the host process: v24.13.0บรรทัดเดียวนั้นใช้แค่ this.constructor.constructor ไม่มี import ไม่มีอะไรผิดปกติ — และมันอ่าน process.version จาก process host ได้ ซึ่งเป็นสิ่งที่ object context ที่ “sandboxed” ไม่เคยได้รับ reference ไปหาเลย eval() และ new Function() ไม่มีเรื่อง isolation เลยแม้แต่น้อย — ทั้งคู่รันใน scope ที่เรียกมันด้วยสิทธิ์เต็มตรง ๆ ถ้าคุณต้องรันโค้ดที่ไม่น่าเชื่อถือจริง ๆ เอกสารของ Node เองระบุชัดว่า vm เพียวๆ ไม่พอ — คุณต้องการ isolation ระดับ OS (process แยกที่ใช้ permission model, container, หรือ project sandboxing เฉพาะทาง) ไม่ใช่เทคนิคระดับ JavaScript
Dependency supply chain
หัวข้อที่มีชื่อว่า “Dependency supply chain”โค้ดส่วนใหญ่ของ Node application ทั่วไปไม่ใช่โค้ดที่ทีมเขียนเอง — มันคือ node_modules checklist ที่ใช้งานได้จริง: npm audit สำหรับ vulnerability ที่รู้จักแล้วใน dependency tree ปัจจุบัน; lockfile ที่ commit ไว้ (package-lock.json) เพื่อให้ “ใช้ได้บนเครื่องฉัน” หมายถึง dependency version เดียวกันทุกที่จริง ๆ ไม่ใช่อะไรก็ตามที่ range ^ บังเอิญ resolve ไปเจอวันนี้; provenance attestation (npm audit signatures, package ที่ publish ด้วย --provenance) เป็นวิธีใหม่ที่กำลังเกิดขึ้นเพื่อยืนยันว่า package ถูก build จาก source ที่ publish จริง ๆ ไม่ได้ถูกแก้ไขหลัง publish; และ --ignore-scripts ตอน install ใน environment ของ CI/build ที่คุณไม่ต้องการ (และไม่อยากเชื่อใจ) ให้ postinstall script ใด ๆ จาก third-party package รันได้ตามใจ
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ลองนึกถึง Node process แบบ default เป็นบ้านที่ทุกประตูไม่ได้ล็อก — ไม่ใช่เพราะใครเลือกแบบนั้น แต่เพราะไม่มีใครติดตั้งกุญแจ --permission คือการเลือกล็อกทุกประตูแล้วแจกกุญแจเฉพาะห้องที่ process นั้นต้องการจริง ๆ vm/eval/new Function ไม่ใช่กุญแจล็อกเลย — มันคือม่าน: มันดูเหมือนกำแพงจากมุมมองของโค้ดเอง แต่อะไรก็ตามที่รู้วิธีเอื้อมผ่านม่านไป (constructor.constructor) จะเจอบ้านเปิดโล่งเหมือนเดิมอยู่ข้างหลังมัน Prototype pollution คือบ้านที่ทุกห้องแอบใช้ blueprint แม่แบบเดียวกัน (Object.prototype) — ทำลาย blueprint ครั้งเดียว ทุกห้องที่สร้างจากมัน ทั้งในอดีตและอนาคต มีการเปลี่ยนแปลงของผู้ทำลายติดไปด้วย
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”Allowlist model ของ --permission คือความปลอดภัยจริง แลกกับการต้องระบุล่วงหน้าว่า process ต้องการอะไรบ้าง — สำหรับ codebase ใหญ่ที่มีอยู่แล้ว การหารายการ path/host/operation ที่มันแตะจริง ๆ ทั้งหมดคืองานที่ไม่เล็กเลยและยังถูกติดป้าย experimental อยู่ การที่ scrypt/pbkdf2 ช้าและกิน memory โดยตั้งใจคือประเด็นทั้งหมดสำหรับ password hashing แต่ต้นทุนเดียวกันนั้นทำให้มันเป็นตัวเลือกที่ผิดทุกที่ที่คุณไม่ได้ป้องกัน offline guessing โดยเฉพาะ (เช่น hash cache key ที่ไม่ secret แต่มี volume สูง) — ใช้ hash ที่เร็วแทนตรงนั้น การ audit dependency (npm audit, lockfile ที่ pin ไว้, --ignore-scripts) มีต้นทุนการดูแลจริง — ต้องมีคนมา review ผล audit แล้วตัดสินว่าอะไรคือความเสี่ยงจริงกับอะไรคือ noise — แต่การข้ามมันไปเลยหมายถึงการเชื่อใจผู้เขียน transitive dependency ทุกคน รวมถึงคนที่คุณไม่เคยเลือก package ของเขาโดยตรงด้วยซ้ำ
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
vmsandbox โค้ดที่ไม่น่าเชื่อถือได้” เอกสารของ Node เองระบุชัดเจนว่าvmไม่ใช่กลไกความปลอดภัยสำหรับโค้ดที่ไม่น่าเชื่อถือ —constructor.constructor(และเทคนิค prototype-chain อื่น) เข้าถึง global ของ host จริงได้จากข้างใน vm context - “Prototype pollution กระทบแค่ object ที่ถูก merge เข้าไป” มันปนเปื้อน
Object.prototypeเอง กระทบ plain object ทุกตัวใน process — รวมถึงตัวที่มีอยู่ก่อนการปนเปื้อนเกิดขึ้นและตัวที่ถูกสร้างหลังจากนั้น ที่ไหนก็ได้ใน codebase - “Lockfile ที่ commit ไว้อย่างเดียวพอสำหรับป้องกัน supply-chain” มัน pin ว่าจะได้ version ไหน ซึ่งจำเป็นแต่ไม่พอ — มันไม่บอกอะไรเลยว่า version เหล่านั้นปลอดภัยหรือเปล่า มันไม่แทนที่
npm audit, การ reviewpostinstallscript, หรือการเช็ก provenance
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ AI สร้างซึ่งสร้าง environment การ evaluate แบบ “จำกัด” เป็นหนึ่งใน pattern ที่อันตรายที่สุดที่จะรับไว้โดยไม่ตรวจสอบ เพราะมันมักดูเหมือนใส่ใจความปลอดภัยโดยตั้งใจ ทั้งที่จริง ๆ แล้วไม่มั่นคงเลยในเชิงพื้นฐาน
🐛 หา bug ในโค้ดนี้
// AI-written "plugin system" -- runs user-submitted expressionsimport vm from 'node:vm';
function runUserExpression(expr, allowedVars) {const context = vm.createContext({ ...allowedVars }); // "sandboxed" -- only exposes allowedVarsreturn vm.runInContext(expr, context);}
// Usage: runUserExpression('a + b', { a: 1, b: 2})เฉลย
Comment อ้างว่านี่ “sandboxed” เพราะ context มีแค่ allowedVars ที่ระบุไว้ชัดเจนเท่านั้น — แต่ตามที่พิสูจน์ไว้ข้างบน expr เข้าถึงได้ไกลเกิน object นั้นผ่าน this.constructor.constructor(...)() ซึ่งคืน Function constructor ตัวจริงและรันด้วยสิทธิ์เต็มของ host process ไม่ว่าจะมีอะไรถูกใส่หรือไม่ใส่ใน context ก็ตาม ไม่มีวิธีแก้ที่ยังคงใช้ vm เปล่า ๆ สำหรับ expression ที่ไม่น่าเชื่อถือจริง ๆ ได้เลย — ทางเลือกจริงคือ: ไม่รับ expression ใดก็ได้จากผู้ใช้เลย (เสนอชุด operation ที่ปลอดภัยและตายตัวแทน), ใช้ expression-evaluator library ที่สร้างมาเฉพาะทางและไม่มีทางเข้าถึง Function/constructor โดยโครงสร้าง, หรือรันการ evaluate ใน process แยกที่ isolated ระดับ OS เต็มรูปแบบ (child process ที่ล็อกไว้แน่น, container) ที่การหนี “sandbox” ไปถึงแค่ process ที่ทิ้งได้นั้น ไม่ใช่ server หลักของคุณ