執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

CSRF 防護

使用 Bun 的內建 API 生成和驗證 CSRF 令牌

Bun.CSRF 生成和驗證 CSRF(跨站請求偽造)令牌。Bun 使用 HMAC 對令牌簽名。每個令牌都包含簽發時間戳和過期時長。

csrf.ts
// 生成一個與請求者會話繫結的令牌
const token = Bun.CSRF.generate("my-secret", { sessionId: "user-session-id" });

// 驗證它
const isValid = Bun.CSRF.verify(token, { secret: "my-secret", sessionId: "user-session-id" });
console.log(isValid); // true

Bun.CSRF.generate()

生成一個 CSRF 令牌。令牌包含一個加密隨機數、簽發時間戳、expiresIn 時長和 HMAC 簽名,並編碼為字串。

generate.ts
const token = Bun.CSRF.generate("my-secret-key");

引數:

  • secret(string,可選)— 用於簽署令牌的金鑰。如果未提供,Bun 會生成一個隨機的記憶體中預設金鑰(每個執行緒唯一)。
  • options(object,可選):
選項型別預設值描述
expiresInnumber86400000令牌過期前的毫秒數。預設為 24 小時。
encodingstring"base64url"令牌編碼格式:"base64"、"base64url" 或 "hex"。
algorithmstring"sha256"HMAC 演算法:"sha256"、"sha384"、"sha512"、"sha512-256"、"blake2b256" 或 "blake2b512"。
sessionIdstring(無)將令牌繫結到請求主體(會話 ID、使用者 ID 或等效識別符號)。只有在呼叫 verify() 時傳入相同的 sessionId,令牌才能通過驗證。

傳回: string — 編碼後的令牌。

generate-options.ts
// 繫結到請求者會話的令牌,1 小時後過期,編碼為 hex
const token = Bun.CSRF.generate("my-secret", {
  sessionId: "user-session-id",
  expiresIn: 60 * 60 * 1000,
  encoding: "hex",
});

// 使用不同的演算法
const token2 = Bun.CSRF.generate("my-secret", {
  sessionId: "user-session-id",
  algorithm: "sha512",
});

Bun.CSRF.verify()

驗證 CSRF 令牌。如果令牌有效且未過期,傳回 true,否則傳回 false。

verify.ts
const isValid = Bun.CSRF.verify(token, { secret: "my-secret-key" });

引數:

  • token(string,必需)— 要驗證的令牌。
  • options(object,可選):
選項型別預設值描述
secretstring(自動)用於簽署令牌的金鑰。如果未提供,則使用與 generate() 相同的記憶體中預設金鑰。
maxAgenumber86400000令牌的最大有效時長(毫秒),獨立於令牌自身的 expiresIn。
encodingstring"base64url"必須與呼叫 generate() 時使用的編碼相匹配。
algorithmstring"sha256"必須與呼叫 generate() 時使用的演算法相匹配。
sessionIdstring(無)必須與呼叫 generate() 時使用的 sessionId 相匹配。繫結到一個主體的令牌無法通過其他主體的驗證。生成時未提供 sessionId 的令牌,在驗證時提供了 sessionId 則無法通過驗證。

傳回: boolean

verify-options.ts
// 驗證一個繫結到請求者會話的令牌
const isValid = Bun.CSRF.verify(token, {
  secret: "my-secret",
  sessionId: "user-session-id",
});

// 強制執行比生成令牌時更短的最大存活時間
const isValid2 = Bun.CSRF.verify(token, {
  secret: "my-secret",
  sessionId: "user-session-id",
  maxAge: 60 * 1000, // 拒絕超過 1 分鐘的令牌
});

在 Bun.serve() 中使用

一種典型模式是在渲染表單時生成令牌,將其嵌入隱藏欄位,並在表單提交時驗證它。將請求者的會話識別符號作為 sessionId 傳遞給這兩次呼叫,這樣令牌只對簽發給它的使用者有效。

server.ts
const SECRET = process.env.CSRF_SECRET || "my-secret";

// 從會話 cookie 中解析請求者的會話識別符號。
// 當訪問者還沒有會話時傳回 null——切勿退回到共享的佔位符,
// 否則所有沒有會話的訪問者都會共享同一個令牌繫結。
function getSessionId(req: Request): string | null {
  return req.headers.get("cookie")?.match(/(?:^|;\s*)session=([^;]+)/)?.[1] ?? null;
}

const server = Bun.serve({
  routes: {
    "/form": req => {
      // 在簽發表單前為每個訪問者建立一個會話,
      // 這樣令牌就會繫結到這個訪問者,而不是其他任何人。
      let sessionId = getSessionId(req);
      const headers = new Headers({ "Content-Type": "text/html" });
      if (!sessionId) {
        sessionId = crypto.randomUUID();
        headers.append("Set-Cookie", `session=${sessionId}; HttpOnly; SameSite=Lax; Path=/`);
      }

      const token = Bun.CSRF.generate(SECRET, { sessionId });

      return new Response(
        `<form method="POST" action="/submit">
          <input type="hidden" name="_csrf" value="${token}" />
          <input type="text" name="message" />
          <button type="submit">傳送</button>
        </form>`,
        { headers },
      );
    },

    "/submit": {
      POST: async req => {
        const sessionId = getSessionId(req);
        const formData = await req.formData();
        const csrfToken = formData.get("_csrf");

        if (!sessionId || typeof csrfToken !== "string" || !Bun.CSRF.verify(csrfToken, { secret: SECRET, sessionId })) {
          return new Response("無效的 CSRF 令牌", { status: 403 });
        }

        return new Response("成功");
      },
    },
  },
});

console.log(`監聽於 ${server.url}`);

預設金鑰

如果在 generate() 和 verify() 中都省略 secret 引數,Bun 會使用每個執行緒生成一次的隨機金鑰。預設金鑰適用於單執行緒應用,但令牌無法跨伺服器或 worker 驗證,也無法在重啟後驗證。

default-secret.ts
// 這兩個呼叫在此執行環境上下文中使用相同的每執行緒預設金鑰。
const token = Bun.CSRF.generate();
const isValid = Bun.CSRF.verify(token); // true

對於生產環境,請始終提供跨基礎設施共享的顯式金鑰。


TypeScript

types.ts
type CSRFAlgorithm = "blake2b256" | "blake2b512" | "sha256" | "sha384" | "sha512" | "sha512-256";

interface CSRFGenerateOptions {
  expiresIn?: number;
  encoding?: "base64" | "base64url" | "hex";
  algorithm?: CSRFAlgorithm;
  sessionId?: string;
}

interface CSRFVerifyOptions {
  secret?: string;
  encoding?: "base64" | "base64url" | "hex";
  algorithm?: CSRFAlgorithm;
  maxAge?: number;
  sessionId?: string;
}

namespace Bun.CSRF {
  function generate(secret?: string, options?: CSRFGenerateOptions): string;
  function verify(token: string, options?: CSRFVerifyOptions): boolean;
}