Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

Security cho web dev: OWASP Top 10 và các lỗi bảo mật phổ biến trong app React

Ếch Trendy
Ếch Trendy

3 tháng trước · 8 phút đọc

Tại sao dev React cần quan tâm đến OWASP?

Nhiều bạn nghĩ bảo mật là việc của backend. Frontend chỉ render UI thôi, có gì nguy hiểm đâu?

Thực tế khác hẳn. Hơn 60% trong các cuộc tấn công web liên quan đến lỗi ở tầng frontend - chủ yếu là XSS, CSRF, và lộ thông tin nhạy cảm qua client-side code. React không tự động miễn nhiễm với những lỗi này.

OWASP Top 10 là danh sách 10 rủi ro bảo mật phổ biến nhất cho ứng dụng web, được cập nhật định kỳ bởi Open Web Application Security Project (OWASP Top 10 chính thức). Đây là tài liệu tham khảo chuẩn trong ngành - nhiều công ty dùng nó làm checklist khi audit bảo mật.

344850 OWASP Top 10 - tài liệu chuẩn mà mọi web dev nên biết

Bài này mình sẽ đi thẳng vào những lỗ hổng nào hay xuất hiện nhất trong app React, code ví dụ cụ thể, và cách fix. Không lý thuyết suông.


XSS (Cross-Site Scripting) - kẻ thù quen mặt nhất

XSS xảy ra khi attacker chèn được script độc hại vào trang web của bạn, và script đó chạy trong trình duyệt của người dùng khác. React có cơ chế tự escape HTML, nhưng có 2-3 trường hợp bạn dễ bỏ qua.

Lỗi phổ biến nhất: dangerouslySetInnerHTML

// ❌ NGUY HIỂM - render HTML từ user input trực tiếp
function Comment({ userInput }) {
  return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
}

// Nếu userInput = '<script>document.cookie</script>'
// Script sẽ chạy ngay khi render

React đặt tên dangerouslySetInnerHTML không phải để dọa bạn - mà để nhắc bạn nhớ sanitize trước khi dùng.

import DOMPurify from 'dompurify';

// ✅ Sanitize trước khi render
function Comment({ userInput }) {
  const clean = DOMPurify.sanitize(userInput);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

344851 Luôn sanitize trước khi dùng dangerouslySetInnerHTML

DOM-based XSS qua URL params

Đây là trường hợp tinh vi hơn:

// ❌ Lấy redirect URL từ query string và dùng trực tiếp
function LoginSuccess() {
  const params = new URLSearchParams(window.location.search);
  const redirect = params.get('next');

  // Attacker gửi: /login?next=javascript:alert(document.cookie)
  return <a href={redirect}>Tiếp tục</a>;
}

// ✅ Chỉ cho phép internal URL
function LoginSuccess() {
  const params = new URLSearchParams(window.location.search);
  const redirect = params.get('next');

  // Kiểm tra URL thuộc domain của mình
  const safeRedirect = redirect?.startsWith('/') ? redirect : '/';
  return <a href={safeRedirect}>Tiếp tục</a>;
}

Thư viện hữu ích: DOMPurify để sanitize HTML, validator.js để validate và escape string an toàn.


CSRF (Cross-Site Request Forgery) - lỗi bị underestimate

CSRF xảy ra khi một trang web độc hại trick người dùng thực hiện action trên trang web khác mà họ đang đăng nhập. Nghe có vẻ phức tạp, nhưng ví dụ sau sẽ rõ hơn.

Người dùng đang đăng nhập vào bank.com. Họ click vào link từ email lạ, trang đó có:

<!-- Trang độc hại chạy ngầm -->
<img src="https://bank.com/transfer?to=attacker&amount=5000000" />

Nếu bank.com chỉ dùng cookie để xác thực và không có CSRF token, request trên sẽ được thực hiện với session cookie hợp lệ của người dùng.

Cách phòng:

Phía backend phải implement CSRF token. Phía React, bạn cần đảm bảo gửi token này trong mỗi request mutation:

// Lấy CSRF token từ cookie (do server set)
function getCsrfToken() {
  return document.cookie
    .split('; ')
    .find(row => row.startsWith('csrftoken='))
    ?.split('=')[1];
}

// Gắn vào mọi POST/PUT/DELETE request
async function apiPost(url, data) {
  return fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRFToken': getCsrfToken(), // Header này server sẽ verify
    },
    body: JSON.stringify(data),
  });
}

344852 CSRF hoạt động nhờ cookie được gửi tự động - CSRF token phá vỡ vòng lặp này

SameSite Cookie - defense in depth

Nếu bạn kiểm soát backend, set SameSite=Strict hoặc SameSite=Lax cho cookie authentication. Điều này ngăn browser gửi cookie khi request xuất phát từ domain khác, giảm đáng kể surface attack của CSRF.


Injection và quản lý secrets - đừng để lộ trên client

Injection (A03 trong OWASP Top 10) thường được nghĩ là SQL Injection ở backend. Nhưng trong React app, có một dạng injection khác hay bị bỏ qua: lộ API key và credentials trong source code frontend.

Mình thấy pattern này khá nhiều:

// ❌ Hard-code API key trong component
const OPENAI_KEY = 'sk-proj-abc123...';
const STRIPE_SECRET = 'sk_live_xyz789...';

async function callAI(prompt) {
  return fetch('https://api.openai.com/v1/chat/completions', {
    headers: { Authorization: `Bearer ${OPENAI_KEY}` },
    // ...
  });
}

Bất kỳ ai mở DevTools đều đọc được key này. Bundle JavaScript không phải nơi lưu secret.

// ✅ Dùng environment variable - CHỈ public key ở đây
// .env file
// REACT_APP_STRIPE_PUBLIC_KEY=pk_live_...
// (KHÔNG bao giờ để sk_ - secret key - ở frontend)

const STRIPE_PUBLIC = process.env.REACT_APP_STRIPE_PUBLIC_KEY;

// Mọi call cần secret key phải đi qua backend của bạn
async function processPayment(data) {
  return fetch('/api/payment', { // Backend proxy call này
    method: 'POST',
    body: JSON.stringify(data),
  });
}

344853 Public key ở frontend được, secret key phải ở backend

Quy tắc đơn giản: Nếu key bắt đầu bằng sk_, secret_, hay tương tự - nó không nên có mặt trong React code. Bao giờ hết.

Ngoài ra, check .gitignore của bạn ngay bây giờ. File .env.local có trong đó chưa? Hơn 39 triệu thông tin mật bị lộ trên GitHub trong năm 2024 (WhiteHat.vn dẫn thống kê của GitHub) vụ lộ thông tin nhạy cảm xảy ra do dev commit file .env lên GitHub công khai.


Security headers và cấu hình đúng cho React app

HTTP Security Headers là lớp phòng thủ dễ implement nhưng nhiều app bỏ qua. Chúng không thay thế code secure, nhưng giảm thiệt hại đáng kể khi có lỗ hổng xảy ra.

Content-Security-Policy (CSP) - quan trọng nhất:

CSP cho phép bạn khai báo nguồn tài nguyên nào được phép load. Nếu XSS xảy ra, CSP có thể chặn script độc hại chạy vì nó không thuộc nguồn được whitelist.

# Ví dụ config Nginx cho React app
add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' https://trusted-cdn.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.yourdomain.com;
" always;

Các headers quan trọng khác:

# Ngăn clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;

# Ngăn MIME sniffing
add_header X-Content-Type-Options "nosniff" always;

# Bắt buộc HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Giới hạn referrer info
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

344854 Security headers là lớp phòng thủ ngoài cùng - rẻ mà hiệu quả

Bạn muốn kiểm tra headers của site mình? Dùng securityheaders.com - paste URL vào là ra report ngay, kèm gợi ý fix.

Một lưu ý với React SPA: Nếu bạn dùng Create React App hay Vite và deploy lên Vercel/Netlify, cấu hình headers thông qua file vercel.json hoặc netlify.toml - không cần Nginx riêng.

// vercel.json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "SAMEORIGIN" }
      ]
    }
  ]
}

Authentication, token storage và broken access control

A01 và A07 trong OWASP 2021 - mất kiểm soát truy cập và lỗi xác thực - chiếm top đầu không phải ngẫu nhiên. Trong React app, hai vấn đề này thường xuất hiện ở chỗ lưu token và kiểm tra quyền.

localStorage vs httpOnly cookie - trade-off thực tế:

Nhiều tutorial dạy lưu JWT vào localStorage. Tiện, nhưng có rủi ro XSS đọc được token.

// ❌ Token trong localStorage - XSS đọc được
localStorage.setItem('token', jwtToken);

// ✅ Dùng httpOnly cookie - JS không đọc được
// Backend set cookie:
// Set-Cookie: token=jwt...; HttpOnly; Secure; SameSite=Strict

// Frontend chỉ cần gọi API - browser tự gửi cookie
fetch('/api/user', { credentials: 'include' });

Tuy nhiên, httpOnly cookie đòi hỏi backend hỗ trợ và phức tạp hơn cho CORS setup. Nếu buộc phải dùng localStorage, ít nhất phải đảm bảo không có XSS vulnerability nào.

344855 localStorage tiện nhưng dễ bị đọc - httpOnly cookie an toàn hơn về mặt XSS

Client-side authorization - lỗi ngây thơ:

// ❌ Chỉ hide UI - không đủ
function AdminPanel({ user }) {
  if (user.role !== 'admin') return null; // Ẩn UI
  return <AdminDashboard />; // Nhưng API /api/admin vẫn gọi được!
}

// ✅ Frontend hide UI + Backend kiểm tra mọi request
// API /api/admin phải verify role ở server, không tin client

Client-side check chỉ là UX - không phải security. Backend phải là nguồn sự thật duy nhất cho authorization.

Nếu bạn đang build app React với authentication từ đầu, khóa Xây Dựng Website với ReactJS cover các pattern cơ bản về state management và routing - base tốt trước khi implement auth flow phức tạp hơn.