0
0
Lập trình
NM

Tăng cường bảo mật cho ứng dụng Vercel: CSP, CORS và Service Workers

Đăng vào 1 tuần trước

• 4 phút đọc

Tăng cường bảo mật cho ứng dụng Vercel: CSP, CORS và Service Workers

Chúng tôi vừa phát hành MVP của Pocket Portfolio (OSS, bảo mật đầu tiên). Bài viết này sẽ trình bày cấu hình CSP, CORS, và Service Worker mà chúng tôi đã sử dụng để giữ cho ứng dụng chạy nhanh và an toàn trên Vercel + Firebase.

Tóm tắt: Giới hạn các nguồn bên thứ ba, cache giao diện người dùng chứ không phải tiền, và đừng để Service Worker chiếm quyền /api/*.


1) Chính sách bảo mật nội dung (CSP)

Chính sách của chúng tôi được định nghĩa trong phần headers của vercel.json. Điểm mấu chốt là chỉ cho phép những gì Firebase Auth thực sự sử dụng (apis.google.com, accounts.google.com, gstatic) và bất kỳ CDN nào mà bạn có ý định sử dụng.

json Copy
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://www.gstatic.com https://*.googleapis.com https://apis.google.com https://accounts.google.com; script-src-elem 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://www.gstatic.com https://*.googleapis.com https://apis.google.com https://accounts.google.com; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; font-src 'self' data: https:; connect-src 'self' https://www.googleapis.com https://*.googleapis.com https://securetoken.google.com https://identitytoolkit.googleapis.com https://firestore.googleapis.com https://*.firebaseio.com https://firebasestorage.googleapis.com https://apis.google.com https://accounts.google.com; frame-src 'self' https://*.google.com https://accounts.google.com https://*.firebaseapp.com https://*.web.app; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
        }
      ]
    }
  ]
}

Lý do:

  • Ngăn chặn tải script không mong muốn và fallout XSS.
  • Cho phép popups/iframes của Google Sign-in hoạt động trong môi trường sản xuất (không có lỗi 400 bí ẩn).

2) CORS cho API Serverless/Edge của bạn

Chỉ công khai những gì trình duyệt cần và chỉ cho trang của bạn.

js Copy
// /api/_cors.js
export const cors = (req, res, { methods = ["GET"], origin = "https://pocketportfolio.app" } = {}) => {
  res.setHeader("Access-Control-Allow-Origin", origin);
  res.setHeader("Vary", "Origin");
  res.setHeader("Access-Control-Allow-Methods", methods.join(","));
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  if (req.method === "OPTIONS") { res.status(204).end(); return true; }
  return false;
};

Sử dụng như sau:

js Copy
// /api/quote.js
import { cors } from "./_cors.js";

export default async function handler(req, res) {
  if (cors(req, res)) return;              // preflight đã được xử lý
  const { ticker } = req.query || {};
  if (!/^[A-Z.\-]{1,7}$/.test(ticker || "")) {
    res.status(400).json({ error: "ticker không hợp lệ" });
    return;
  }
  // lấy dữ liệu từ upstream → chuẩn hóa → phản hồi
  res.setHeader("Cache-Control", "public, max-age=5, stale-while-revalidate=25");
  res.status(200).json({ price: 123.45, ts: Date.now() });
}

3) Một Service Worker không làm hỏng ứng dụng của bạn

Cache các shell (CSS/JS/icons) và các điều hướng trong /app/*. Không bao giờ chặn /api/*.

js Copy
/* /app/service-worker.js */
const SW_VERSION = "pp-v9";
const SHELL = [
  "/app/", "/app/index.html", "/app/style.css", "/app/app.js",
  "/app/manifest.webmanifest", "/brand/pp-monogram.svg"
];

self.addEventListener("install", (e) => {
  e.waitUntil(caches.open(SW_VERSION).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting()));
});

self.addEventListener("activate", (e) => {
  e.waitUntil(
    caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== SW_VERSION).map((k) => caches.delete(k))))
      .then(() => self.clients.claim())
  );
});

self.addEventListener("fetch", (e) => {
  const url = new URL(e.request.url);
  if (url.origin !== location.origin) return;           // bỏ qua bên thứ ba
  if (!url.pathname.startsWith("/app/")) return;        // giữ phạm vi chặt chẽ
  if (url.pathname.startsWith("/api/")) return;         // không bao giờ cache APIs

  // Tài nguyên tĩnh → cache-first
  if (/\.(css|js|mjs|map|svg|png|jpg|jpeg|webp|ico|woff2?)$/i.test(url.pathname)) {
    e.respondWith(
      caches.match(e.request).then((hit) =>
        hit ||
        fetch(e.request).then((res) => {
          caches.open(SW_VERSION).then((c) => c.put(e.request, res.clone()));
          return res;
        })
      )
    );
    return;
  }

  // Điều hướng → network-first, cache fallback
  if (e.request.method === "GET") {
    e.respondWith(
      fetch(e.request).then((res) => {
        caches.open(SW_VERSION).then((c) => c.put(e.request, res.clone()));
        return res;
      }).catch(() => caches.match(e.request).then((hit) => hit || caches.match("/app/index.html")))
    );
  }
});

Đăng ký chỉ trong môi trường sản xuất:

html Copy
<script>
if ("serviceWorker" in navigator && !/localhost|127\.0\.0\.1/.test(location.hostname)) {
  navigator.serviceWorker.register("/app/service-worker.js?sw=9").catch(()=>{});
}
</script>

4) Củng cố thêm (drop-ins)

  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: geolocation=(), microphone=(), camera=()
  • Cache tài sản: Cache-Control: public, max-age=31536000, immutable
  • Giới hạn tỷ lệ cho các điểm nóng (Edge middleware hoặc util)

Chúng tôi đang xây dựng gì

Pocket Portfolio là một trình theo dõi danh mục đầu tư OSS, không có môi giới. Thêm giao dịch hoặc nhập một CSV nhỏ. Giá trực tiếp, P/L, giao diện sạch sẽ.

Không phải là lời khuyên đầu tư. Chỉ dành cho nghiên cứu/giáo dục.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào