บทความ › ความปลอดภัย

แยกกุญแจตามหน้าที่ ระบบเดียวใช้ 4 แบบ หลุดดอกหนึ่ง อีกสามดอกยังปลอดภัย

เครื่องที่ส่งข้อมูล คนที่ล็อกอิน คนที่ดูหน้าสาธารณะ และคนที่ขอตามไม้ ต้องใช้กุญแจคนละแบบ ออกแบบ API key ผูกกับบัญชี session cookie และลิงก์สาธารณะ จากระบบที่รันจริง

กลาง อ่าน 6 นาที 16 ตุลาคม 2569

อ่านจบแล้วจะรู้
  • แยกกุญแจตามว่าใครใช้และใช้ทำอะไร กุญแจแต่ละดอกทำได้แค่งานของมัน หลุดดอกหนึ่งไม่เปิดอีกดอก
  • API key ที่ดีมีคำนำหน้าให้จำได้ สุ่มด้วยตัวสุ่มที่ปลอดภัย และผูกกับสิ่งที่มันมีสิทธิ์เขียน
  • cookie ของ session ต้องเป็น HttpOnly, Secure และ SameSite เพื่อลดผลจาก XSS และ CSRF

ระบบบันทึกผลเทรดของเรามีคนและเครื่องเข้ามาใช้สี่แบบ โปรแกรมเทรดที่ส่งข้อมูลทุกไม่กี่วินาที เจ้าของบัญชีที่ล็อกอินดูแดชบอร์ด คนทั่วไปที่เปิดลิงก์ดูผลงาน และผู้ตามที่ดึงไม้ไปเปิดตาม ถ้าใช้กุญแจแบบเดียวกันหมด กุญแจที่ฝังอยู่ในโปรแกรมบนเครื่องคนอื่นจะเปิดได้ทุกประตู บทความนี้เล่าวิธีแยกกุญแจทั้งสี่แบบ และเหตุผลของแต่ละการตัดสินใจ เขียนจากมุมคนออกแบบระบบให้ปลอดภัย

หลักการ: กุญแจหนึ่งดอก งานหนึ่งอย่าง

คิดแบบอาคารสำนักงาน พนักงานส่งของได้บัตรเข้าแค่ห้องรับของ แม่บ้านเข้าได้ทุกห้องแต่เปิดตู้เซฟไม่ได้ ผู้มาติดต่อนั่งได้แค่ล็อบบี้ บัตรหายหนึ่งใบ ก็ยกเลิกแค่ใบนั้น

หลักการ: แยกกุญแจตามว่า ใคร ใช้ และใช้ทำ อะไร กุญแจแต่ละดอกทำได้แค่งานของมัน

ใครใช้อะไรทำอะไรได้ทำอะไรไม่ได้
โปรแกรมเทรดAPI key ใน header X-Api-Keyส่งข้อมูลของบัญชีเดียวอ่านข้อมูล เข้าแดชบอร์ด
เจ้าของบัญชีอีเมล + รหัสผ่าน → session cookieดูและจัดการบัญชีของตัวเองเขียนข้อมูลเทรดแทนโปรแกรม
คนทั่วไปลิงก์สาธารณะ /p/<slug>ดูสถิติแบบอ่านอย่างเดียวเห็นเลขบัญชีเต็ม แก้อะไรก็ได้
ผู้ตามไม้token สำหรับแชร์ ใน X-Copy-Tokenอ่านรายการไม้ที่ถืออยู่ส่งข้อมูล เข้าแดชบอร์ด
ใครกุญแจประตูที่เปิดได้โปรแกรมเทรดX-Api-Key: fxk_…ส่งข้อมูล 1 บัญชีเจ้าของบัญชีcookie: fxsessแดชบอร์ดตัวเองคนทั่วไป/p/<slug>ดูอย่างเดียวผู้ตามไม้X-Copy-Tokenอ่านไม้ที่ถือ
กุญแจหนึ่งดอก ประตูหนึ่งบาน หลุดดอกหนึ่ง อีกสามดอกไม่ได้รับผลกระทบ

กุญแจที่ 1: API key ของเครื่องที่ส่งข้อมูล

key ของเรามีหน้าตาแบบนี้

fxk_Q7mZp2Kx9vLr4Tn8Wc1Yh5Bd6Sa3

มีคำนำหน้า fxk_ ทำให้จำได้ทันทีเมื่อเห็นใน log ไฟล์ตั้งค่า หรือในโค้ดที่เผลอ commit ขึ้น git เครื่องมือสแกนหาความลับก็ตั้งกฎจับได้ง่าย

สุ่มด้วยตัวสุ่มที่ปลอดภัย ใช้ crypto/rand ของ Go ไม่ใช่ math/rand ซึ่งเดาค่าถัดไปได้ ส่วนสุ่มยาว 28 ตัวอักษร

func IngestKey() string { return "fxk_" + randString(28) } // randString ใช้ crypto/rand

ผูกกับบัญชีเทรดเดียว นี่คือส่วนที่สำคัญที่สุด ครั้งแรกที่โปรแกรมส่งข้อมูลเข้ามา ระบบจะผูก key กับเลขบัญชีเทรดในข้อมูลนั้น หลังจากนั้นถ้ามีข้อมูลเลขบัญชีอื่นส่งมาด้วย key เดียวกัน ระบบตอบ 409 Conflict และไม่บันทึก

if payloadLogin != 0 && payloadLogin != acct.Login {
    writeError(w, http.StatusConflict,
        "account login mismatch: this key belongs to a different MT account")
    return false
}

ทำไมต้องผูก เพราะระบบนี้คือระบบ ยืนยันผลเทรด ถ้า key ของบัญชีหนึ่งส่งข้อมูลของอีกบัญชีมาปนได้ ผลที่แสดงก็เชื่อไม่ได้ ไม่ว่าจะเกิดจากคนตั้งใจโกง หรือแค่ก็อปไฟล์ตั้งค่าไปใช้กับอีกบัญชีโดยลืมเปลี่ยน key

กุญแจที่ 2: session ของคนที่ล็อกอิน

คนล็อกอินด้วยอีเมลกับรหัสผ่าน รหัสผ่านเก็บเป็น bcrypt ไม่เก็บตัวจริง

hash, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
ok := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil

ล็อกอินผ่านแล้ว ระบบสร้าง session token สุ่ม 40 ตัวอักษร เก็บฝั่งเซิร์ฟเวอร์ และส่งให้เบราว์เซอร์เป็น cookie ที่ตั้งค่าแบบนี้

http.SetCookie(w, &http.Cookie{
    Name: "fxsess", Value: token, Path: "/",
    HttpOnly: true,                  // JavaScript อ่านไม่ได้
    Secure:   secure,                // ส่งเฉพาะทาง HTTPS
    SameSite: http.SameSiteLaxMode,  // ไม่แนบไปกับคำขอจากเว็บอื่นแบบ POST
    MaxAge:   30 * 24 * 60 * 60,     // 30 วัน
})
  • HttpOnly ถ้าวันหนึ่งหน้าเว็บมีช่องโหว่ XSS สคริปต์ที่ถูกฝังก็อ่าน cookie นี้ไม่ได้
  • Secure cookie ไม่ถูกส่งผ่าน HTTP ธรรมดาที่ดักฟังได้
  • SameSite=Lax เว็บอื่นส่งฟอร์มมาสั่งงานแทนผู้ใช้ไม่ได้ ลดความเสี่ยง CSRF
  • ออกจากระบบ = ลบ session ฝั่งเซิร์ฟเวอร์ ไม่ใช่แค่ลบ cookie ในเบราว์เซอร์ token ที่ถูกขโมยไปก่อนหน้าจะใช้ต่อไม่ได้

กุญแจที่ 3: ลิงก์สาธารณะ

เจ้าของบัญชีเลือกเปิดหน้าสาธารณะได้ หน้านี้ใช้ slug สุ่ม 10 ตัวอักษร เช่น /p/k3Xm9QaZ2w ไม่ใช้เลขบัญชีหรือ id ที่เรียงลำดับ คนจึงไล่เดาหน้าของคนอื่นไม่ได้

ข้อมูลในหน้านี้อ่านอย่างเดียว และ ซ่อนเลขบัญชีเทรด เหลือแค่ 4 ตัวท้าย

func maskLogin(login int64) string {
    s := strconv.FormatInt(login, 10)
    return "••••" + s[len(s)-4:]
}

เลขบัญชีเต็มเป็นข้อมูลที่ไม่จำเป็นต่อการดูผลงาน และอาจถูกใช้ประกอบการหลอกลวงได้ จึงไม่แสดง

กุญแจที่ 4: token สำหรับแชร์ให้ผู้ตาม

ผู้ตามไม้ไม่ต้องมีบัญชีในระบบเลย ได้แค่ token ที่เจ้าของส่งให้ token นี้อ่านได้อย่างเดียว และเจ้าของ เปลี่ยน token ใหม่ ได้ทุกเมื่อ เพื่อตัดผู้ตามทั้งหมดในครั้งเดียว ระบบลบ cache ของ token เก่าทันที ไม่รอให้หมดอายุ (เล่าละเอียดในบทความเรื่อง cache ทางร้อน)

รายการตรวจของกุญแจที่ดี

  • แยกตามหน้าที่ ไม่มีกุญแจไหนทำได้ทุกอย่าง
  • มีคำนำหน้า จำได้เมื่อหลุดไปอยู่ผิดที่
  • สุ่มด้วยตัวสุ่มที่ปลอดภัย และยาวพอที่จะเดาไม่ได้
  • ผูกกับขอบเขต บัญชีเดียว สิทธิ์เดียว
  • เปลี่ยนใหม่ได้ โดยไม่กระทบกุญแจอื่น
  • เก็บแค่ hash สำหรับ key ที่ใช้ยืนยันตัว แสดงตัวเต็มครั้งเดียวตอนสร้าง
  • ส่งใน header ไม่ใช่ใน URL เพราะ URL ไปโผล่ใน log ของเซิร์ฟเวอร์และ proxy
  • จำกัดอัตราการลอง กันการเดา

ข้อผิดพลาดที่เจอบ่อย

  • ใช้รหัสผ่านของคนฝังในโปรแกรมหรือสคริปต์อัตโนมัติ
  • สุ่ม token ด้วย Math.random() หรือ math/rand
  • cookie ไม่มี HttpOnly เพราะหน้าเว็บ “อยากอ่าน” token เอง
  • ออกจากระบบแค่ลบ cookie ฝั่งเบราว์เซอร์ session ฝั่งเซิร์ฟเวอร์ยังใช้ได้
  • ใช้ id เรียงลำดับเป็นลิงก์สาธารณะ ใครก็ไล่เปลี่ยนเลขดูของคนอื่นได้

คำถามที่พบบ่อย

ทำไมไม่ให้เครื่องส่งข้อมูลล็อกอินด้วยอีเมลกับรหัสผ่านเหมือนคน

เพราะรหัสผ่านต้องไปฝังอยู่ในโปรแกรมบนเครื่องอื่น ถ้าหลุด คนได้ไปจะเข้าหน้าจัดการทั้งหมดได้ API key แยกจำกัดสิทธิ์แค่ส่งข้อมูลของบัญชีเดียว และยกเลิกได้โดยไม่ต้องเปลี่ยนรหัสผ่าน

ควรเก็บ API key ในฐานข้อมูลยังไง

แนวทางที่ดีคือเก็บเฉพาะค่า hash ของ key แบบเดียวกับรหัสผ่าน แสดง key ตัวเต็มให้ผู้ใช้เห็นครั้งเดียวตอนสร้าง ถ้าฐานข้อมูลหลุด คนได้ไปจะไม่ได้ key ที่ใช้งานได้

คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ

อ่านต่อ

#Securityกลาง

Passkeys ล็อกอินไม่ต้องใช้รหัสผ่าน: เซิร์ฟเวอร์ เก็บอะไร ต้องเช็กอะไร

Passkeys (WebAuthn) ไม่มีความลับฝั่งเซิร์ฟเวอร์และหลอก phishing ไม่ได้ ขั้นลงทะเบียน/ล็อกอิน ข้อมูลที่ต้องเก็บ โค้ด SimpleWebAuthn, py_webauthn และข้อผิดพลาดที่พบบ่อย

อ่าน 15 นาที
#Securityกลาง

รับ Webhook ให้ปลอดภัย ตรวจ HMAC กัน replay ตอบเร็ว และกัน event ซ้ำ

endpoint รับ webhook เปิดให้ทั้งโลกยิงได้ ตรวจลายเซ็น HMAC บน raw body แบบ constant-time เช็กเวลากัน replay ตอบ 2xx ทันที แล้วกัน event ซ้ำและสลับลำดับ

อ่าน 12 นาที
#Securityกลาง

กัน CSRF โดยไม่ต้องใช้ token ด้วย Sec-Fetch-Site และ Origin (Go 1.25)

เบราว์เซอร์ยุคนี้บอกเซิร์ฟเวอร์เองว่าคำขอมาจากเว็บอื่นหรือเปล่า เช็กหัว Sec-Fetch-Site กับ Origin แทน CSRF token ได้ วิธีทำใน Go 1.25 และข้อแม้ที่ต้องรู้

อ่าน 10 นาที