ลดความยุ่งยากในการใช้คลาวด์ พูดคุยกับเจ้าหน้าที่

Home ตั้งค่า HTTP Security Headers ให้เว็บไซต์

ตั้งค่า HTTP Security Headers ให้เว็บไซต์

HTTP Security Headers คือคำสั่งที่เซิร์ฟเวอร์ส่งไปพร้อมหน้าเว็บ เพื่อบอกเบราว์เซอร์ว่าควรป้องกันผู้ใช้อย่างไร เช่น ต้องใช้ HTTPS เสมอ ห้ามนำหน้าเว็บไปฝังใน iframe ของเว็บอื่น หรือห้ามเดาชนิดไฟล์เอง Header เหล่านี้ไม่ได้แทนการอัปเดตระบบหรือเขียนโค้ดให้ปลอดภัย แต่ช่วยปิดช่องทางโจมตีฝั่งเบราว์เซอร์หลายแบบ เช่น Clickjacking และ XSS บางรูปแบบ

บทความนี้อธิบาย Header ที่ควรมี ความหมาย ค่าที่แนะนำ และวิธีตั้งบน Nginx, Apache, Plesk และ IIS พร้อมข้อควรระวังที่ทำให้เว็บพังได้ถ้าตั้งเร็วเกินไป

สิ่งที่ต้องเตรียม

  • เว็บไซต์ที่ใช้ HTTPS ได้สมบูรณ์แล้ว (ทุกหน้าและทุก Subdomain ที่เกี่ยวข้อง)
  • สิทธิ์แก้ไฟล์ตั้งค่า Web Server หรือไฟล์ .htaccess หรือสิทธิ์เข้า Plesk
  • สำเนาไฟล์ตั้งค่าเดิม และช่วงเวลาที่ทดสอบเว็บได้หลังเปลี่ยน

Header ที่แนะนำและความหมาย

Headerค่าที่แนะนำเป็นจุดเริ่มต้นป้องกันอะไร
Strict-Transport-Securitymax-age=300 แล้วค่อยเพิ่มเป็น max-age=31536000บังคับให้เบราว์เซอร์ใช้ HTTPS เสมอ ป้องกันการดักเปลี่ยนเป็น HTTP
X-Content-Type-Optionsnosniffห้ามเบราว์เซอร์เดาชนิดไฟล์ เช่นตีความไฟล์ข้อความเป็นสคริปต์
X-Frame-OptionsSAMEORIGINClickjacking โดยห้ามเว็บอื่นนำหน้าเราไปฝังใน iframe
Referrer-Policystrict-origin-when-cross-originไม่ส่ง URL เต็ม (ซึ่งอาจมีข้อมูลส่วนตัว) ไปให้เว็บอื่น
Permissions-Policycamera=(), microphone=(), geolocation=()ปิดการใช้ฟีเจอร์ของเบราว์เซอร์ที่เว็บไม่ได้ใช้
Content-Security-Policyเริ่มแบบ Content-Security-Policy-Report-Onlyกำหนดว่าโหลดสคริปต์ รูป ฟอนต์ จากที่ไหนได้บ้าง ลดผลกระทบของ XSS

Header X-XSS-Protection เลิกใช้แล้ว เบราว์เซอร์รุ่นปัจจุบันไม่รองรับ ไม่จำเป็นต้องตั้ง ถ้าเครื่องมือตรวจบางตัวแนะนำ ให้ตั้งเป็น 0 หรือไม่ตั้งเลย ส่วน X-Frame-Options ใช้ร่วมกับ frame-ancestors 'self' ใน CSP ได้ เบราว์เซอร์รุ่นใหม่จะยึด CSP เป็นหลัก

ขั้นตอนที่ 1: ตรวจ Header ที่มีอยู่ตอนนี้

curl -sI https://example.com/

ดูว่ามี Header ใดในตารางอยู่แล้วบ้าง เพราะแผงควบคุม, ปลั๊กอินความปลอดภัย หรือ CDN อาจส่งมาให้แล้ว ถ้าตั้งซ้ำ เบราว์เซอร์จะได้รับสองค่า ซึ่งบาง Header (เช่น CSP) จะถูกนำมาใช้ร่วมกันแบบเข้มขึ้นจนเว็บพังได้

ขั้นตอนที่ 2: ตั้งค่าบน Nginx

ใส่ในบล็อก server { } ที่ฟัง 443 ของเว็บ

add_header Strict-Transport-Security "max-age=300" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

คำว่า always ทำให้ส่ง Header แม้หน้าตอบกลับเป็น Error เช่น 404 หรือ 500

ข้อควรระวังที่พบบ่อยที่สุด: ถ้าบล็อก location ใดมี add_header ของตัวเอง (เช่นตั้ง Cache-Control ให้ไฟล์รูป) Header ทั้งหมดจากระดับ server จะไม่ถูกส่งใน location นั้นเลย วิธีแก้คือเก็บ Header ไว้ในไฟล์แยก แล้ว include ทั้งในระดับ server และใน location ที่มี add_header ของตัวเอง

sudo nano /etc/nginx/snippets/security-headers.conf

ย้ายบรรทัด add_header ทั้งห้าบรรทัดข้างต้นไปไว้ในไฟล์นี้ ในบล็อก server เหลือเพียง include snippets/security-headers.conf; และใน location ที่มี Header ของตัวเองให้เขียนแบบนี้

location ~* \.(jpg|jpeg|png|webp|svg|css|js)$ {
    include snippets/security-headers.conf;
    add_header Cache-Control "public, max-age=2592000";
}

ตรวจและโหลดค่าใหม่

sudo nginx -t
sudo systemctl reload nginx

บน AlmaLinux/Rocky ไม่มีโฟลเดอร์ snippets มาให้ สร้างเองด้วย sudo mkdir -p /etc/nginx/snippets แล้ว include ด้วย Path เต็ม

ขั้นตอนที่ 3: ตั้งค่าบน Apache หรือไฟล์ .htaccess

ต้องเปิดโมดูล mod_headers ก่อน บน Ubuntu ใช้

sudo a2enmod headers
sudo systemctl reload apache2

บน AlmaLinux/Rocky โมดูลนี้เปิดอยู่แล้ว ตรวจด้วย httpd -M | grep headers จากนั้นใส่ใน Virtual Host หรือด้านบนของไฟล์ .htaccess

<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=300"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>

ใช้ set ไม่ใช่ add เพื่อแทนที่ค่าเดิมแทนการส่งซ้ำ ถ้าแก้ Virtual Host ให้รัน apachectl configtest ก่อน reload

ขั้นตอนที่ 4: บน Plesk และ IIS

บน Plesk ที่เปิด Proxy mode ใช้วิธีของ Apache ใน .htaccess ได้ หรือไปที่ Websites & Domains เลือกโดเมน เปิด Apache & nginx Settings แล้วใส่คำสั่ง add_header ในช่อง Additional nginx directives ส่วน HSTS อาจตั้งจากส่วนขยาย SSL It! ได้ในบางเวอร์ชัน ถ้าตั้งที่นั่นแล้ว อย่าตั้งซ้ำในที่อื่น

บน IIS ใส่ในไฟล์ web.config ของเว็บ

<system.webServer>
  <httpProtocol>
    <customHeaders>
      <add name="X-Content-Type-Options" value="nosniff" />
      <add name="X-Frame-Options" value="SAMEORIGIN" />
      <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
    </customHeaders>
  </httpProtocol>
</system.webServer>

หรือตั้งผ่าน IIS Manager ที่ไอคอน HTTP Response Headers ของเว็บ สำหรับ HSTS บน IIS 10 รุ่น Windows Server 2019 ขึ้นไป ตั้งได้ในหน้าตั้งค่า HSTS ของ Site

ขั้นตอนที่ 5: เพิ่ม HSTS อย่างระมัดระวัง

HSTS ถูกจดจำในเบราว์เซอร์ของผู้เข้าชมตามจำนวนวินาทีที่กำหนด ถ้าตั้งไว้หนึ่งปีแล้ววันหนึ่ง HTTPS ใช้ไม่ได้ (เช่นใบรับรองหมดอายุ) ผู้เข้าชมจะกดข้ามคำเตือนไม่ได้เลย จึงควรเพิ่มทีละขั้น

  1. เริ่มที่ max-age=300 (5 นาที) ทดสอบทุกหน้า
  2. เพิ่มเป็น max-age=86400 (1 วัน) แล้ว max-age=2592000 (30 วัน)
  3. เมื่อมั่นใจแล้วจึงใช้ max-age=31536000 (1 ปี)
  4. ใส่ includeSubDomains เฉพาะเมื่อ ทุก Subdomain ใช้ HTTPS ได้ รวมถึง Subdomain ภายในอย่าง mail.example.com
  5. preload ทำให้ชื่อโดเมนถูกฝังในเบราว์เซอร์ ถอนออกได้ยากและใช้เวลานาน ใส่เฉพาะเมื่อตั้งใจจริงเท่านั้น

ดูวิธีเฝ้าวันหมดอายุของใบรับรองได้ที่ ตรวจสอบ SSL Certificate และวันหมดอายุ

ขั้นตอนที่ 6: เริ่ม Content-Security-Policy แบบ Report-Only

CSP ทรงพลังที่สุดแต่ก็ทำให้เว็บพังง่ายที่สุด เพราะต้องระบุทุกแหล่งที่เว็บโหลดสคริปต์ เช่น Google Analytics, Tag Manager, แชต หรือแผนที่ เริ่มจากโหมดรายงานก่อน ซึ่งเบราว์เซอร์จะแจ้งเตือนใน Console แต่ไม่บล็อกจริง

add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'" always;

เปิดหน้าเว็บหลายๆ หน้า แล้วดูข้อความ Content-Security-Policy ในแท็บ Console ของ DevTools เพิ่มแหล่งที่ถูกต้องลงใน Policy จนไม่มีข้อความเตือน จากนั้นจึงเปลี่ยนชื่อ Header เป็น Content-Security-Policy ให้มีผลจริง เว็บ WordPress ที่ใช้ปลั๊กอินจำนวนมากมักต้องใช้เวลาปรับนาน ถ้าเวลาจำกัด ตั้งเฉพาะ frame-ancestors 'self' ไว้ก่อนก็ได้ประโยชน์แล้ว

ตรวจสอบผลลัพธ์

curl -sI https://example.com/ | grep -iE 'strict-transport|x-content-type|x-frame|referrer-policy|permissions-policy|content-security'

ควรเห็นแต่ละ Header เพียงหนึ่งบรรทัด ทดสอบกับไฟล์รูปหรือ CSS และกับหน้าที่ไม่มีอยู่จริงด้วย เพื่อยืนยันว่า Header ถูกส่งในทุกกรณี

curl -sI https://example.com/wp-content/themes/x/style.css | grep -i x-content-type
curl -sI https://example.com/not-found-test | grep -i x-frame

จากนั้นเปิดเว็บในเบราว์เซอร์ ทดลองฟังก์ชันสำคัญ เช่น ฟอร์มติดต่อ, ตะกร้าสินค้า, หน้าชำระเงิน และหลังบ้าน

ปัญหาที่พบบ่อย

Header หายไปในบางไฟล์ (Nginx)

เป็นเรื่องการสืบทอด add_header ที่อธิบายในขั้นตอนที่ 2 ให้ include ไฟล์ Header ในทุก location ที่มี add_header ของตัวเอง

ฟีเจอร์ที่ฝังจากเว็บอื่นใช้ไม่ได้

ถ้าเว็บของคุณต้องถูกฝังใน iframe ของอีกโดเมนหนึ่ง (เช่นระบบจองที่ฝังในเว็บพาร์ทเนอร์) SAMEORIGIN จะบล็อก ให้ลบ X-Frame-Options แล้วใช้ CSP frame-ancestors 'self' https://partner.example.net แทน

ตั้ง HSTS แล้ว Subdomain เข้าไม่ได้

เกิดจากใส่ includeSubDomains ขณะที่บาง Subdomain ยังไม่มี SSL แก้โดยติดตั้ง SSL ให้ Subdomain นั้น การลดค่า max-age จะมีผลกับผู้เข้าชมเมื่อเบราว์เซอร์ของเขากลับมาเยี่ยมชมผ่าน HTTPS อีกครั้งเท่านั้น

Header เป็นเพียงส่วนหนึ่งของความปลอดภัย ดูภาพรวมเพิ่มเติมได้ที่ เช็กลิสต์ความปลอดภัยเว็บไซต์ WordPress หากต้องการความช่วยเหลือ ติดต่อทีมงาน THAI DATA CLOUD ได้ที่ https://thaidata.cloud/contact/

ผู้ให้บริการคลาวด์ไทย
เพื่อธุรกิจของคนไทย

"มุ่งมั่น" และ "มั่นคง"
พร้อมรับมือทุกการเติบโต
Trust Cloud
คลาว์ที่ปลอดภัย
คือรากฐานที่มั่นคง
cloud security