Same-origin policy และความปลอดภัยของ storage
Origin คืออะไร?
หัวข้อที่มีชื่อว่า “Origin คืออะไร?”Origin คือการผสมกันของสามองค์ประกอบ:
- Scheme —
httpsหรือhttp - Host — domain หรือ IP address
- Port — ระบุชัดเจน (
:8080) หรือ implicit (443 สำหรับ HTTPS, 80 สำหรับ HTTP)
ทั้งสามต้องตรงกันเพื่อให้ URL สองตัวถือว่าเป็น origin เดียวกัน ตารางด้านล่างแสดงตัวอย่างเทียบกับ https://example.com:
| URL | Origin เดียวกัน? | เหตุผล |
|---|---|---|
https://example.com/page | ใช่ | scheme, host, port เหมือนกัน |
https://example.com:443/x | ใช่ | Port 443 เป็น implicit สำหรับ HTTPS |
http://example.com | ไม่ | scheme ต่างกัน |
https://www.example.com | ไม่ | host ต่างกัน (subdomain) |
https://example.com:8080 | ไม่ | port ต่างกัน |
https://api.example.com | ไม่ | host ต่างกัน |
คุณสามารถอ่าน origin ของหน้าปัจจุบันด้วย JavaScript:
console.log(window.location.origin);// เช่น "https://example.com"Same-origin policy แยก storage อย่างไร
หัวข้อที่มีชื่อว่า “Same-origin policy แยก storage อย่างไร”browser บังคับใช้ origin isolation สำหรับทุก client-side store: localStorage, sessionStorage, IndexedDB, Cache API และ OPFS ล้วนถูกแบ่งตาม origin script ที่รันบน https://example.com ไม่สามารถอ่านหรือเขียน storage ของ https://other.com ได้ browser จะไม่อนุญาตเลย
นี่ไม่ใช่สิ่งที่คุณตั้งค่า แต่ถูกบังคับใช้ที่ระดับ platform โดยไม่มีทางปิดใช้งาน
flowchart LR
subgraph OriginA["Origin A — https://example.com"]
direction TB
SA1[(localStorage)]
SA2[(IndexedDB)]
SA3[(Cache API)]
end
subgraph OriginB["Origin B — https://other.com"]
direction TB
SB1[(localStorage)]
SB2[(IndexedDB)]
SB3[(Cache API)]
end
OriginA -.->|blocked by SOP| OriginB HTTP vs HTTPS: origin ต่างกัน, store ต่างกัน
หัวข้อที่มีชื่อว่า “HTTP vs HTTPS: origin ต่างกัน, store ต่างกัน”เนื่องจาก scheme เป็นส่วนหนึ่งของ origin http://example.com และ https://example.com จึงเป็น สอง origins ที่แตกต่างกัน มี storage buckets แยกกันอย่างสมบูรณ์ ข้อมูลที่เขียนลง localStorage บน HTTP จะไม่ปรากฏบน HTTPS และในทางกลับกัน
นี่เป็นแหล่งที่มาของความสับสนบ่อยครั้งระหว่างการพัฒนาในเครื่อง: หากคุณทดสอบบน HTTP แล้วเปลี่ยนเป็น HTTPS (หรือกลับกัน) ข้อมูลที่เก็บไว้ก่อนหน้าจะดูเหมือนหายไป แต่จริง ๆ แล้วข้อมูลยังอยู่ เพียงแต่ถูกเก็บภายใต้ origin key ที่ต่างกัน
Cross-origin iframes
หัวข้อที่มีชื่อว่า “Cross-origin iframes”หน้าเว็บสามารถฝัง <iframe> จาก origin อื่นได้ scripts ของ iframe นั้นรันในบริบทของ origin ของ iframe จึงอ่านและเขียน storage ของ origin นั้น ไม่ใช่ storage ของหน้าหลัก ไม่มีวิธีที่หน้าหลักและ iframe จะแชร์ localStorage โดยตรงข้าม origins ที่ต่างกัน
ความเสี่ยง XSS: อย่าเก็บ secrets ใน Web Storage
หัวข้อที่มีชื่อว่า “ความเสี่ยง XSS: อย่าเก็บ secrets ใน Web Storage”Cross-site scripting (XSS) คือการโจมตีที่ JavaScript อันตรายถูก inject และรันบนหน้าของคุณ script ใด ๆ ที่รันบน origin ของคุณ รวมถึง scripts ที่ถูก inject สามารถอ่าน localStorage และ sessionStorage ทั้งหมดของคุณได้ด้วยการเรียกเพียงครั้งเดียว:
// script ที่ attacker inject สามารถทำสิ่งนี้ได้:const stolen = JSON.stringify(localStorage);// จากนั้นส่ง `stolen` ไปยัง server ที่ attacker ควบคุมซึ่งหมายความว่าคุณต้อง ไม่ เก็บ access tokens, refresh tokens, API keys หรือ passwords ใน localStorage หรือ sessionStorage หาก attacker พบ XSS vector ใด ๆ พวกเขาสามารถดูด secrets ทั้งหมดที่เก็บไว้ได้อย่างเงียบ ๆ
ใช้ HttpOnly cookies สำหรับ session tokens แทน
หัวข้อที่มีชื่อว่า “ใช้ HttpOnly cookies สำหรับ session tokens แทน”Cookies ที่มี flag HttpOnly ไม่สามารถอ่านได้ด้วย JavaScript เลย แม้แต่ scripts ที่ถูก inject browser จะแนบไปกับ requests แต่ซ่อนจาก document.cookie เมื่อรวมกับ Secure (HTTPS-only) และ SameSite=Strict หรือ SameSite=Lax แล้ว HttpOnly cookies เป็นที่ที่เหมาะสมสำหรับ session tokens
// นี่คือสิ่งที่ server ตั้งค่า — JS ไม่สามารถอ่านได้:// Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Laxข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| Same-origin isolation (บังคับโดย browser) | ป้องกัน origin อื่นอ่าน/เขียน storage ข้าม origin โดยอัตโนมัติ ไม่ต้อง config เอง | ปิดใช้งานไม่ได้ ทำให้แชร์ข้อมูลข้าม subdomain ต้องพึ่งกลไกอื่น เช่น server-side hand-off |
HttpOnly cookies สำหรับ session tokens | XSS อ่านไม่ได้เลยแม้ script จะถูก inject สำเร็จ | อ่าน/เขียนจาก client-side JavaScript ไม่ได้ ต้องพึ่ง server ตั้งค่าและอ่านค่าเสมอ |
เก็บ tokens ใน localStorage/sessionStorage | เข้าถึงจาก JavaScript ได้ง่าย เหมาะกับ SPA ที่ต้องแนบ token เอง | XSS จุดเดียวดูด token ทั้งหมดออกไปได้ในครั้งเดียว |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- คิดว่า subdomain เดียวกันแชร์ storage กันได้ —
https://app.example.comกับhttps://example.comเป็นคนละ origin เพราะ host ต่างกัน แม้จะเป็น domain เดียวกันก็ตาม - เข้าใจว่าเปลี่ยนจาก HTTP เป็น HTTPS แล้วข้อมูลเดิมยังอยู่ — scheme เป็นส่วนหนึ่งของ origin การสลับ HTTP/HTTPS จึงสร้าง storage partition ใหม่ทั้งหมด ข้อมูลเก่าไม่ได้หายไปไหน แค่ดูเหมือนหายเพราะอยู่คนละ origin
- เก็บ access token หรือ API key ไว้ใน
localStorage— ถ้าเจอ XSS แม้แค่จุดเดียว attacker ก็ดึง token ทั้งหมดออกไปได้ทันที ควรใช้HttpOnlycookie แทนสำหรับ session tokens
💡 ตัวอย่างจากของจริง
Auth0 / Firebase Auth — แนะนำให้เก็บ session/refresh token ใน
HttpOnlycookie แทนlocalStorageโดยเฉพาะสำหรับแอปที่ handle ข้อมูล sensitive เพื่อลดผลกระทบจาก XSSCookie-consent banners (GDPR/PDPA) — ต้องตรวจสอบ origin ของ third-party script ที่ฝังใน iframe อย่างระมัดระวัง เพราะ script เหล่านั้นเข้าถึง storage ได้แค่ origin ของตัวเอง ไม่ใช่ storage ของหน้าหลัก