ข้ามไปยังเนื้อหา

Same-origin policy และความปลอดภัยของ storage

Origin คือการผสมกันของสามองค์ประกอบ:

  1. Schemehttps หรือ http
  2. Host — domain หรือ IP address
  3. Port — ระบุชัดเจน (:8080) หรือ implicit (443 สำหรับ HTTPS, 80 สำหรับ HTTP)

ทั้งสามต้องตรงกันเพื่อให้ URL สองตัวถือว่าเป็น origin เดียวกัน ตารางด้านล่างแสดงตัวอย่างเทียบกับ https://example.com:

URLOrigin เดียวกัน?เหตุผล
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"

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
แต่ละ origin มี storage partition ที่แยกจากกัน — การอ่านข้าม origin ถูกบล็อก

เนื่องจาก scheme เป็นส่วนหนึ่งของ origin http://example.com และ https://example.com จึงเป็น สอง origins ที่แตกต่างกัน มี storage buckets แยกกันอย่างสมบูรณ์ ข้อมูลที่เขียนลง localStorage บน HTTP จะไม่ปรากฏบน HTTPS และในทางกลับกัน

นี่เป็นแหล่งที่มาของความสับสนบ่อยครั้งระหว่างการพัฒนาในเครื่อง: หากคุณทดสอบบน HTTP แล้วเปลี่ยนเป็น HTTPS (หรือกลับกัน) ข้อมูลที่เก็บไว้ก่อนหน้าจะดูเหมือนหายไป แต่จริง ๆ แล้วข้อมูลยังอยู่ เพียงแต่ถูกเก็บภายใต้ origin key ที่ต่างกัน

หน้าเว็บสามารถฝัง <iframe> จาก origin อื่นได้ scripts ของ iframe นั้นรันในบริบทของ origin ของ iframe จึงอ่านและเขียน storage ของ origin นั้น ไม่ใช่ storage ของหน้าหลัก ไม่มีวิธีที่หน้าหลักและ iframe จะแชร์ localStorage โดยตรงข้าม origins ที่ต่างกัน

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 ทั้งหมดที่เก็บไว้ได้อย่างเงียบ ๆ

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
ตัวเลือกBenefitCost
Same-origin isolation (บังคับโดย browser)ป้องกัน origin อื่นอ่าน/เขียน storage ข้าม origin โดยอัตโนมัติ ไม่ต้อง config เองปิดใช้งานไม่ได้ ทำให้แชร์ข้อมูลข้าม subdomain ต้องพึ่งกลไกอื่น เช่น server-side hand-off
HttpOnly cookies สำหรับ session tokensXSS อ่านไม่ได้เลยแม้ 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 ทั้งหมดออกไปได้ทันที ควรใช้ HttpOnly cookie แทนสำหรับ session tokens

💡 ตัวอย่างจากของจริง

Auth0 / Firebase Auth — แนะนำให้เก็บ session/refresh token ใน HttpOnly cookie แทน localStorage โดยเฉพาะสำหรับแอปที่ handle ข้อมูล sensitive เพื่อลดผลกระทบจาก XSS

Cookie-consent banners (GDPR/PDPA) — ต้องตรวจสอบ origin ของ third-party script ที่ฝังใน iframe อย่างระมัดระวัง เพราะ script เหล่านั้นเข้าถึง storage ได้แค่ origin ของตัวเอง ไม่ใช่ storage ของหน้าหลัก

สามองค์ประกอบใดที่รวมกันกำหนด browser origin?
หน้าเว็บเก็บข้อมูลใน localStorage บน http://example.com เมื่อ site ย้ายไป HTTPS จะเกิดอะไรกับข้อมูลนั้น?
ทำไมจึงไม่ควรเก็บ session tokens ใน localStorage?