Chuyển tới nội dung chính

Hướng dẫn bảo mật

Các best practices để bảo mật khi tích hợp Payment Gateway PayerScan.

Bảo mật API Key

Không bao giờ để lộ API Key

API Key dùng để xác thực các request đến API PayerScan. Nó không bao giờ được hiển thị cho người dùng cuối.

// ❌ SAI - Gọi API từ frontend (API Key bị lộ cho mọi người)
fetch('https://api.payerscan.com/payment/crypto', {
headers: { 'x-api-key': 'EQnhBYpknGAP...' } // API Key bị lộ!
})

// ✅ ĐÚNG - Gọi API từ backend server
// Frontend gọi API nội bộ của bạn
fetch('/api/create-payment', { body: { amount: 100 } })

// Backend của bạn gọi PayerScan (API Key an toàn ở server)
app.post('/api/create-payment', async (req, res) => {
const result = await fetch('https://api.payerscan.com/payment/crypto', {
headers: { 'x-api-key': process.env.PAYERSCAN_API_KEY }
})
res.json(result)
})

Lưu trữ API Key an toàn

# ✅ Dùng environment variables
PAYERSCAN_API_KEY=EQnhBYpknGAP...

# ❌ Không hardcode trong code
const API_KEY = 'EQnhBYpknGAP...' # KHÔNG LÀM THẾ NÀY

Lưu ý: Không bao giờ commit API Key vào hệ thống quản lý phiên bản (Git). Thêm .env vào file .gitignore.

Rotate API Key định kỳ

  • Đổi API Key nếu nghi ngờ bị lộ
  • Vào trang quản lý Store → Generate New API Key
  • Cập nhật API Key mới trên server ngay lập tức
  • API Key cũ sẽ bị vô hiệu hóa ngay sau khi tạo key mới

Bảo mật Webhook

Sử dụng HTTPS

Luôn dùng HTTPS cho callback_url để mã hóa dữ liệu truyền tải và ngăn chặn tấn công man-in-the-middle.

// ✅ ĐÚNG - Dùng HTTPS
callback_url: 'https://your-server.com/webhook/payment'

// ❌ SAI - Dùng HTTP (dữ liệu không được mã hóa, dễ bị đánh cắp)
callback_url: 'http://your-server.com/webhook/payment'

Xác thực webhook request

Luôn xác thực rằng webhook request thực sự đến từ PayerScan:

app.post('/webhook/payment', express.json(), async (req, res) => {
try {
const { merchant_id, api_key, trans_id, request_id, status, amount, transaction_hash } = req.body

// 1. Xác minh merchant_id (cả completed và expired)
if (merchant_id !== process.env.PAYERSCAN_MERCHANT_ID) {
return res.status(401).json({ error: 'Invalid merchant' })
}

// 2. Xác minh api_key (chỉ completed — expired không có api_key)
if (status === 'completed' && api_key !== process.env.PAYERSCAN_API_KEY) {
return res.status(401).json({ error: 'Invalid API Key' })
}

// 3. Tìm đơn hàng theo request_id (nếu có) hoặc trans_id
let order = null
if (request_id) {
order = await db.orders.findOne({ request_id })
}
if (!order) {
order = await db.orders.findOne({ trans_id })
}
if (!order) {
return res.status(404).json({ error: 'Order not found' })
}

// 4. Chỉ xử lý đơn hàng đang ở trạng thái có thể cập nhật (idempotent)
const processableStatuses = ['waiting', 'pending', 'processing']
if (!processableStatuses.includes(order.status)) {
return res.status(200).json({ message: 'Already processed' })
}

// 5. Xác minh amount khớp (chỉ completed — chống giả mạo)
if (status === 'completed') {
if (parseFloat(amount) !== parseFloat(order.amount)) {
return res.status(400).json({ error: 'Amount mismatch' })
}
}

// 6. Kiểm tra transaction_hash chưa được dùng (chống replay)
if (status === 'completed') {
const existingTx = await db.orders.findOne({ transaction_hash })
if (existingTx) {
return res.status(409).json({ error: 'Transaction hash already used' })
}
}

// 7. Atomic update — kèm filter status để chống race condition
if (status === 'completed') {
const result = await db.orders.update(
{ trans_id, status: { $in: processableStatuses } },
{ status: 'completed', transaction_hash }
)
if (result.modifiedCount === 0) {
return res.status(200).json({ message: 'Already processed' })
}
} else if (status === 'expired') {
const result = await db.orders.update(
{ trans_id, status: { $in: processableStatuses } },
{ status: 'expired' }
)
if (result.modifiedCount === 0) {
return res.status(200).json({ message: 'Already processed' })
}
}

res.status(200).json({ success: true })
} catch (error) {
console.error('Webhook processing error:', error)
res.status(500).json({ error: 'Internal server error' })
}
})

Xử lý idempotent

Webhook có thể được gửi lại tối đa 5 lần. Đảm bảo xử lý nhiều lần vẫn an toàn:

// ✅ ĐÚNG - Chỉ cập nhật đơn hàng đang ở trạng thái có thể xử lý
const processableStatuses = ['waiting', 'pending', 'processing']
if (!processableStatuses.includes(order.status)) {
return res.status(200).json({ message: 'Already processed' })
}

// ❌ SAI - Không kiểm tra, có thể cộng tiền nhiều lần
await addBalance(user, amount) // Nếu webhook gửi 2 lần → cộng 2 lần!

Bảo mật dữ liệu

Không log dữ liệu nhạy cảm

// ❌ SAI - Log API Key
console.log('Request with API Key:', req.headers['x-api-key'])

// ✅ ĐÚNG - Chỉ log những gì cần thiết
console.log('Payment created:', { trans_id, amount, status })

Validate input

PayerScan đã validate input ở phía server, nhưng bạn cũng nên validate ở phía mình:

// Validate amount (phải là số dương, tối đa 1,000,000 USD)
const amount = parseFloat(req.body.amount)
if (isNaN(amount) || amount <= 0 || amount > 1000000) {
return res.status(400).json({ error: 'Invalid amount' })
}

// Validate callback_url format
if (req.body.callback_url) {
try {
const url = new URL(req.body.callback_url)
if (url.protocol !== 'https:') {
console.warn('callback_url nên dùng HTTPS để bảo mật')
}
} catch {
return res.status(400).json({ error: 'Invalid callback_url' })
}
}

Bảo mật phía máy chủ PayerScan

PayerScan triển khai nhiều lớp bảo mật ở phía server:

  • Chống SSRF: Hệ thống xác thực tất cả giá trị callback_url trước khi gửi webhook. Các IP nội bộ/riêng tư và URL đáng ngờ bị chặn tự động.
  • Rate Limiting: Các endpoint API được bảo vệ bằng giới hạn tốc độ burst và sustained để ngăn chặn lạm dụng. Xem Rate Limits để biết chi tiết.
  • Validation Input: Tất cả tham số request được validate bằng schema chặt chẽ (phạm vi amount, định dạng URL, giới hạn độ dài chuỗi).
  • Webhook Timeout: Webhook callback có timeout 10 giây. Nếu server của bạn không phản hồi trong 10 giây, lần thử bị đánh dấu thất bại và sẽ được thử lại.

Checklist bảo mật

Trước khi go-live

  • API Key được lưu trong environment variables (không hardcode)
  • API Key không bị lộ ở frontend/client-side code
  • File .env đã thêm vào .gitignore
  • Webhook endpoint sử dụng HTTPS
  • Webhook xác minh merchant_id cho cả completed và expired
  • Webhook xác minh api_key cho webhook completed
  • Webhook tìm đơn hàng theo request_id hoặc trans_id
  • Xử lý webhook idempotent (không xử lý trùng lặp)
  • Webhook xác minh amount khớp với đơn hàng
  • Webhook kiểm tra transaction_hash chưa dùng (chống replay)
  • Webhook sử dụng atomic update kèm filter status (chống race condition)
  • Không log dữ liệu nhạy cảm (API Key, private key)
  • Validate input cho amount, URLs

Định kỳ

  • Review access logs cho webhook endpoint thường xuyên
  • Giám sát các webhook request trái phép hoặc đáng ngờ
  • Rotate API Key định kỳ hoặc khi bị lộ
  • Cập nhật dependencies có security patches
  • Kiểm tra webhook endpoint phản hồi trong 10 giây

Xử lý sự cố bảo mật

Nếu API Key bị lộ

  1. Ngay lập tức vào trang quản lý Store → Generate New API Key
  2. Cập nhật API Key mới trên tất cả servers
  3. Review các API logs gần đây để tìm request trái phép
  4. Liên hệ support nếu phát hiện giao dịch đáng ngờ

Nếu phát hiện webhook giả mạo

  1. Xác minh webhook handler đã kiểm tra merchant_idapi_key
  2. Kiểm tra logs xem có amount không khớp hoặc transaction_hash trùng lặp
  3. Block IP nguồn (nếu xác định được)
  4. Thêm IP allowlisting nếu có thể
  5. Liên hệ support để báo cáo sự cố

Liên hệ support

Nếu có vấn đề bảo mật khẩn cấp, liên hệ ngay: