- Published on
รวมระบบการเขียนฟอร์มเพื่อกันบอต
- Authors
- Name
- Samiti
เกริ่น
เว็บไซต์แทบทุกเว็บจะต้องมีสิ่งหนึ่งเรียกว่าฟอร์มไว้สำหรับกรอกข้อมูล ไม่ว่จะเป็นการกรอกข้อมูลเพื่อติดต่อ แสดงความคิดเห็น หรือใส่ข้อมูลส่วนตัว และไม่ว่าอย่างไรก็ตามแต่ เว็บมาสเตอร์ต้องเผชิญหน้ากับสิ่งที่เป็นคู่ปรับนั้นคือ "บอต" โปรแกรมอัตโนมัติที่พร้อมทำตามคำสั่งนาย ผู้สร้างโปรแกรม
โปรแกรมบอต ที่ถูกใช้คำสั่งกรอกฟอร์มตามเว็บไซต์ต่าง ๆ นั้น เรียกว่าบอตพวกนี้ว่า บอตอัตโนมัติ (Automated Bots) หรือผู้เขียนขอบัญญัติใหม่เรียกว่า ฟอร์มบอต (Form Bots) เนื่องจากมันถูกออกแบบมาเพื่อกรอกข้อมูลในฟอร์มโดยอัตโนมัติโดยตรง บอตเหล่านี้สามารถถูกตั้งโปรแกรมให้กรอกข้อมูลตามที่กำหนดไว้ล่วงหน้า หรือตอบสนองต่อฟอร์มต่าง ๆ ที่พบในเว็บไซต์ เพราะบอตพวกนี้ต้องทำหน้าที่อย่างเดียวก็คือ การกรอกข้อมูลซ้ำ ๆ เช่น การลงทะเบียนหลายบัญชี การสมัครเข้าร่วมกิจกรรม หรือส่งข้อมูลเชิญชวนหลอกลวง
จะสังเกตได้ว่ามันถูกนำมาใช้ในทางที่ดีและในทางที่ไม่ดี โดยเฉพาะ เช่น การส่งสแปมหรือการกรอกข้อมูลปลอมในฟอร์ม ดังที่กล่าวมาข้างต้น และสร้างความน่ารำคาญให้กับเว็บมาสเตอร์ และใช้ในทางที่ไม่เหมาะสม
4 เทคนิคการยับยั้งบอตกรอกฟอร์มอัตโนมัติ
1. Honeypot Field เทคนิคหม้อน้ำผึ้ง
ฮันนี่พ็อต เป็นกับดักทางไซเบอร์ที่ใช้ดักแฮกเกอร์ให้หลงทางเข้าไปอยู่ในโซนที่เตรียมไว้เพื่อจับตาและเฝ้าดูพฤติกรรม หลักการทำงานเกี่ยวกับบอตคือ บอตแสกนฟิลด์ต่าง ๆ ทันทีและจะเผลอกรอกฟิลด์พิเศษ หรือกับดับทีเตรียมไว้ให้ หากกรอกได้สำเร็จนั้นหมายความว่าเป็นบอตแน่นอน เพราะฟิลด์ดังกล่าว ไม่สามารถมองเห็นได้ด้วยตาหรือผ่าน UI เนื่องจากเป็น input ประเภท hidden หรือ ใช้ css class ซ่อนเอาไว้อีกที
วิธีสร้างคือ
- สร้าง state ชื่อ trap
const [trap, setTrap] = useState<string | null>(null)
- สร้างฟิลด์ HoneyPot
<input type="text" className="hidden" name="trap" onChange={(e) => setTrap(e.target.value)} />
- ช่วงกด submit
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (trap) {
///บันทึกข้อมูลหรือทำอะไรก็ได้ตามสบาย
return
}
}
2. Time-Based Validation เทคนิคตรวจจับเวลากรอกฟอร์ม
ในการดักจับบอตส่วนใหญ่พวกเราจะพบว่า ช่วงเวลากด submit ฟอร์มแทบไม่พบเลย หายไปอย่างไร้ร่องรอย ไร้ตัวตน ดังนั้น เทคนิคดักจับบอตฟอร์มท่านี้ก็คือ การตรวจจับเวลา นั้นเอง
- สร้าง state
const [startTime, setStartTime] = useState<number>(0)
- ตั้งค่าช่วงเข้าหน้าแรกใน useEffect ซึ่งมีหน้าที่จัดการข้อมูลช่วงเรียกหน้าใช้งาน โดยจะตั้งค่า setStartTime ด้วยเวลาปัจจุบันขณะเข้าใช้งาน
useEffect(() => {
setStartTime(Date.now())
}, [])
- ตรวจสอบช่วง submit form หากตรวจพบว่าน้อยกว่า 3 วินาที แสดงว่าเป็นบอต
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const currentTime: number = Date.now()
const timeElapsed = (currentTime - startTime) / 1000
if (timeElapsed < 3) {
return
}
}
3. Fingerprinting ตรวจลายนิ้วมือ
มีหลายข้อที่สามารถตรวจจับผ่านลายนิ้วมือได้คือ navigator.webdriver เพราะโดยปกติ ถ้าเข้าใช้งานโดยมนุษย์จะไม่มีค่า true แต่หากเป็น web driver object ดังกล่าจะขึ้นแจ้งเตือน
เพิ่มเติมก็คือ หลายคนใช้ Selenium เป็นเครื่องมือที่ถูกสั่งการโดย Script โดยจะนำคำสั่งดังกล่าวควบคุมเว็บบราวเซอร์อีกที เช่น Chrome, Firefox, และ Safari เป็นต้น เพื่อทำ Web scrapling, การทดสอบระบบ หรือ สั่งทำการซ้ำซากทั้งหลาย
และค่า object ที่ส่งมาสามารถสังเกต ข้อมูลที่แปลกประหลาด ได้เช่น
- navigator.permissions ทำไมต้องขอ permissions???
- window.chrome ในการใช้ web selenium จะไม่ปรากฏค่านี้ 100%
- navigator.languages เป็น array ว่างนั้นเป็นตัวบ่งชี้ว่าใช้บอตมาอีกเช่นเดียวกัน
ขั้นตอนทำต่อไปนี้
- สร้าง state
const [webdriver, setWebDriver] = useState<boolean>(false)
- ตรวจสอบครั้งแรกที่เข้าใช้งาน หรือช่วง render หน้าเว็บแรกด้วย useEffect จากนั้นตรวจสอบ webdriver ใด ๆ ด้วย navigator หากเป็น webdriver หรือแอบเข้าใช้ Selenium จะขึ้น true ทำให้ ตรงกับเงื่อนไข if else
นอกจากนี้ยังพิจารณา window.outerWidth และ window.outerHeight เพราะระบบบอตเวลาเปิดการทำงานจะเป็น headless กรอบจอทั้งโปรแกรมจะเหลือค่า 0 นั้นทำให้เห็นจุดอ่อน และข้อบกพร่องของระบบนี้ ทำให้ตรวจสอบได้ง่าย
หากครบเงื่อนไขตามที่กล่าวมา webdriver จะต้องเป็น true
useEffect(() => {
if (navigator.webdriver || window.outerWidth === 0 || window.outerHeight === 0) {
setWebDriver(true)
}
}, [])
- ช่วง Submit Form ให้ดักตรงนี้เลย
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (webDriver) {
return
}
}
4. CSRF Tokens การตรวจสอบด้วย CSRF Token
เป็นที่ทราบดีว่า request ยิง api ไปยัง route หลังบ้าน แต่ทราบไหมว่า หากนำ api/mail ยิงเข้าตรง ๆ ก็สามารถส่งได้โดยไม่ผ่านฟอร์ม วิธีแก้จุดนี้ และป้องกันบอตไม่ให้ส่งได้คือ ให้สร้างโทเค็นหน้าบ้านและหลังบ้านให้ตรงกัน เพียงเท่านี้ก็แก้ปัญหาได้แล้ว
นอกจากนี้บอตยังมีข้อเสียเปรียบเพราะการใช้ระบบ automate เข้ามาไม่สามารถเรียก Token ในส่วนของหน้าบ้านได้อย่างถูกต้อง เหมือนการเข้าใช้งาน ตามปกติโดยผู้ใช้งาน
แนวทางก็คือ สร้าง Token ส่งให้กลับมา พร้อมตั้งค่า Headers ใน Http โดยสร้างตัวแปล csrfToken ตามด้วยค่าที่ถูกสร้างขึ้นมา ซึ่งหน้าตาจะใกล้เคียงกับตัวอย่างด้านล่าง
Cookie: PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1
และเมื่อผู้ใช้งานกรอกฟอร์มแล้ว จะทำการนำ Token ที่ส่งกลับมาเทียบกับ Headers ใน Http Request สำหรับการศึกษาเพิ่มเติมสามารถเข้าไปค้นคว้าได้ที่เอกสารต้นทาง เพิ่มเติม
เป็นวิธีที่มีศักยภาพสูงเพื่อช่วยป้องกัน Cross-Site Request Forgery หรือ "การปลอมแปลงคำขอระหว่างเว็บไซต์"
- สร้างไฟล์ utils.js เพื่อจัดการ CSRF Token โดยเฉพาะ
หากใครสนใจสร้างเอง ตามตัวอย่างด้านล่าง
// utils/csrf.ts
import { randomBytes } from 'crypto'
import { serialize } from 'cookie'
export function generateCsrfToken() {
return randomBytes(32).toString('hex')
}
export function setCsrfCookie(response: NextResponse, csrfToken: string) {
response.cookies.set('csrfToken', csrfToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
})
}
- สร้าง Route สำหรับร้องขอ CSRF
// app/api/auth/csrf-token/route.ts
import { generateCsrfToken, setCsrfCookie } from 'app/utils/csrf'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest) {
const csrfToken = await generateCsrfToken()
const response = NextResponse.json({ csrfToken })
setCsrfCookie(response, csrfToken)
return response
}
- แจกโทเค็น เมื่อเรียกฟอร์ม
// pages/form.tsx
'use client'
import { useEffect, useState, FormEvent } from 'react'
export default function ContactForm() {
const [csrfToken, setCsrfToken] = useState<string>('')
useEffect(() => {
const fetchCsrfToken = async () => {
const res = await fetch('/api/auth/csrf-token')
const data = await res.json()
setCsrfToken(data.csrfToken)
}
fetchCsrfToken()
}, [])
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData()
formData.append('username', e.currentTarget.username.value)
formData.append('email', e.currentTarget.email.value)
formData.append('csrfToken', e.currentTarget.csrfToken.value)
for (const [key, value] of formData.entries()) {
console.log(`${key}: ${value}`)
}
await fetch('/api/submit-form', {
method: 'POST',
body: formData,
})
}
return (
<form className="my-12 flex flex-col gap-y-4" onSubmit={handleSubmit}>
csrfToken: {csrfToken}
<input name="csrfToken" type="hidden" value={csrfToken} />
<input name="username" type="text" placeholder="Username" required />
<input name="email" type="email" placeholder="Email" required />
<button className="btn border-teal-200 bg-emerald-600 py-4" type="submit">
Submit
</button>
</form>
)
}
- ตรวจสอบโทเค็นหลังบ้าน
// app/api/submit-form/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
if (req.method !== 'POST') {
return NextResponse.json({ error: 'Method Not Allowed' }, { status: 405 })
}
const formData = await req.formData()
const csrfTokenFromFormData = formData.get('csrfToken')
const csrfTokenFromCookies = req.cookies.get('csrfToken')?.value
if (csrfTokenFromFormData !== csrfTokenFromCookies) {
return NextResponse.json({ message: 'CSRF token mismatch' }, { status: 403 })
}
return NextResponse.json({ message: 'Form submitted successfully' }, { status: 200 })
}
export const config = {
// Allow only POST method
matcher: '/api/submit-form',
}
สรุป
การตรวจสอบบอตฟอร์ม ที่มักเป็นปัญหาในการเปิดเว็บไซต์เป็นเรื่องที่สามารถยับยั้งและลดปริมาณอันเป็นต้นเหตุของความวุ่นวายได้ด้วย เทคนิคและวิธีการต่าง ๆ ตามที่ยกตัวอย่าง ผู้เขียนแนะนำให้ลองทำตามและนำไปประยุกต์ใช้ตามแต่ต้องการ