mcpField
S
security-bun

security-bun

github.com/igorwarzocha安全122104 次安装38 周前更新
代码仓库

关于 security-bun

面向 Bun 应用开发者的安全审计技能,涵盖命令与 SQL 注入、路径遍历、服务配置及 Bun 特有风险。包含快速搜索命令和加固清单。

主要功能

  1. 01检查 Shell 与命令注入风险
  2. 02识别 SQL 拼接和参数化问题
  3. 03审查请求校验、CORS 与主机绑定
  4. 04检查路径遍历和 WebSocket 认证
  5. 05审查环境变量与 bunfig.toml
  6. 06提供审计命令和加固清单

适用场景

  1. 01审计 Bun.shell 和 Bun.spawn 的用户输入处理
  2. 02检查 bun:sqlite 查询是否存在注入风险
  3. 03评估 Bun.serve 的路径、CORS 和网络暴露问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/igorwarzocha/opencode-workflows --skill security-bun

安装后在 Claude Code 中输入/security-bun确认已加载。

附带文件scripts/scan.sh

详细介绍

针对 Bun 运行时应用的安全审计模式,涵盖 Shell 注入、SQL 注入、服务器安全和 Bun 特定漏洞。

Bun 最容易踩的坑:Shell 转义与原始 Shell

Bun 的 Shell $ 是一个默认会进行转义的标签模板。如果绕过转义(通过原始模式),用户输入就可能导致命令注入。

import { $ } from "bun";

const userInput = "hello; rm -rf /";

// ✓ SAFE: Tagged template - automatically escapes
await $`echo ${userInput}`;
// Executes: echo 'hello; rm -rf /'

// ❌ CRITICAL: Spawning a new shell (bypasses Bun escaping)
await $`bash -c "echo ${userInput}"`;
// The nested shell interprets user input as code
参数注入(即使已转义)

即使使用安全的标签模板,也可能遭受参数注入:

import { $ } from "bun";

// ❌ HIGH: Argument injection via -- prefix
const userRepo = "--upload-pack=id>/tmp/pwned";
await $`git ls-remote ${userRepo} main`;
// The -- prefix makes it a command-line argument, not a value

// ✓ Validate input format before use
const userRepo = getUserInput();
if (!userRepo.match(/^https?:\/\//)) {
  throw new Error("Invalid repository URL");
}
await $`git ls-remote ${userRepo} main`;

// ✓ Or use -- to end argument parsing
await $`git ls-remote -- ${userRepo} main`;

bun:sqlite SQL 注入

sql 是一个会对值进行参数化的标签模板。如果手动构建 SQL 字符串,仍然可能遭受注入。

import { sql } from "bun";

const userId = "1 OR 1=1";

// ❌ CRITICAL: Function call - SQL injection!
await sql(`SELECT * FROM users WHERE id = ${userId}`);
// Executes: SELECT * FROM users WHERE id = 1 OR 1=1

// ✓ SAFE: Tagged template - parameterized query
await sql`SELECT * FROM users WHERE id = ${userId}`;
// Executes: SELECT * FROM users WHERE id = $1 with params ['1 OR 1=1']
bun:sqlite 数据库类
import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite");
const userInput = "'; DROP TABLE users; --";

// ❌ CRITICAL: String interpolation
db.run(`INSERT INTO logs VALUES ('${userInput}')`);

// ✓ SAFE: Parameterized with .run()
db.run("INSERT INTO logs VALUES (?)", [userInput]);

// ✓ SAFE: Prepared statements
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
stmt.get(userInput);

// ✓ SAFE: Query with parameters
db.query("SELECT * FROM users WHERE email = ?").get(userInput);

Bun.serve() 安全性

缺少请求验证
// ❌ No input validation
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    const file = url.searchParams.get("file");
    return new Response(Bun.file(`./uploads/${file}`)); // Path traversal!
  },
});

// ✓ Validate and sanitize
import { join, basename, resolve } from "path";

Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    const file = url.searchParams.get("file");
    
    // Sanitize filename
    const safeName = basename(file ?? "");
    const uploadsDir = resolve("./uploads");
    const filePath = resolve(join(uploadsDir, safeName));
    
    // Verify path is within uploads directory
    if (!filePath.startsWith(uploadsDir)) {
      return new Response("Forbidden", { status: 403 });
    }
    
    return new Response(Bun.file(filePath));
  },
});
请求大小限制(DoS 防护)
// ❌ No body size limit (large uploads can exhaust memory)
Bun.serve({
  fetch(req) {
    return new Response("ok");
  },
});

// ✓ Set a max request body size
Bun.serve({
  maxRequestBodySize: 1_000_000, // 1 MB
  fetch(req) {
    return new Response("ok");
  },
});
CORS 配置
// ❌ Wide open CORS
Bun.serve({
  fetch(req) {
    return new Response("data", {
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Credentials": "true", // Dangerous combo!
      },
    });
  },
});

// ✓ Explicit origin allowlist
const ALLOWED_ORIGINS = ["https://app.example.com"];

Bun.serve({
  fetch(req) {
    const origin = req.headers.get("Origin");
    const corsHeaders: Record<string, string> = {};
    
    if (origin && ALLOWED_ORIGINS.includes(origin)) {
      corsHeaders["Access-Control-Allow-Origin"] = origin;
      corsHeaders["Access-Control-Allow-Credentials"] = "true";
    }
    
    return new Response("data", { headers: corsHeaders });
  },
});
主机绑定
// ❌ Exposed to network (sometimes unintentional)
Bun.serve({
  hostname: "0.0.0.0", // Accessible from any network interface
  port: 3000,
  fetch(req) { /* ... */ },
});

// ✓ Localhost only for development
Bun.serve({
  hostname: "127.0.0.1", // Only local access
  port: 3000,
  fetch(req) { /* ... */ },
});

Bun.spawn() 命令注入

// ❌ CRITICAL: User input in command array (can still be dangerous)
const filename = userInput; // Could be "--version" or other flags
Bun.spawn(["convert", filename, "output.png"]);

// ❌ CRITICAL: Shell execution with user input
Bun.spawn(["sh", "-c", `convert ${userInput} output.png`]);

// ✓ Validate input first
const filename = userInput;
if (!filename.match(/^[a-zA-Z0-9_-]+\.(jpg|png|gif)$/)) {
  throw new Error("Invalid filename");
}
Bun.spawn(["convert", filename, "output.png"]);

// ✓ Use -- to prevent flag injection
Bun.spawn(["convert", "--", filename, "output.png"]);

Bun.file() 和 Bun.write() 路径遍历

// ❌ HIGH: Path traversal
const userFile = req.query.file; // "../../etc/passwd"
const content = await Bun.file(`./uploads/${userFile}`).text();

// ❌ HIGH: Writing to arbitrary paths
await Bun.write(`./data/${userFile}`, content);

// ✓ Sanitize paths
import { join, basename, resolve } from "path";

const UPLOADS_DIR = resolve("./uploads");

function getSafePath(userInput: string): string {
  const safeName = basename(userInput);
  const fullPath = resolve(join(UPLOADS_DIR, safeName));
  
  if (!fullPath.startsWith(UPLOADS_DIR)) {
    throw new Error("Invalid path");
  }
  
  return fullPath;
}

const content = await Bun.file(getSafePath(userFile)).text();

Bun.password(安全,但需检查用法)

// ✓ Bun.password.hash is secure by default (uses argon2)
const hash = await Bun.password.hash(password);

// ✓ Verify passwords
const isValid = await Bun.password.verify(password, hash);

// ⚠️ But check: is it actually being used?
// Common vibecoding mistake: storing plaintext anyway

// ❌ Storing plaintext
db.run("INSERT INTO users (password) VALUES (?)", [password]);

// ✓ Storing hash
const hash = await Bun.password.hash(password);
db.run("INSERT INTO users (password_hash) VALUES (?)", [hash]);

环境变量

// Bun.env is the same as process.env

// ❌ Secrets in client-facing code
// If using Bun with a bundler, check what gets bundled

// ✓ Server-only access
const apiKey = Bun.env.API_KEY;
if (!apiKey) {
  throw new Error("API_KEY not configured");
}

// Check bunfig.toml for any exposed variables
bunfig.toml 安全性
# Check for suspicious configurations

[install]
# ❌ Disabling lockfile = supply chain risk
save-lockfile = false

# ❌ Allowing arbitrary registries
registry = "http://malicious-registry.com"

[run]
# ❌ Disabling sandbox (if applicable)

WebSocket 安全性

Bun.serve({
  fetch(req, server) {
    if (req.headers.get("upgrade") === "websocket") {
      // ❌ No auth check before upgrade
      server.upgrade(req);
      return;
    }
  },
  websocket: {
    message(ws, message) {
      // ❌ Broadcasting without auth
      ws.publish("chat", message);
    },
  },
});

// ✓ Authenticate before upgrade
Bun.serve({
  fetch(req, server) {
    if (req.headers.get("upgrade") === "websocket") {
      const token = req.headers.get("Authorization");
      const user = await verifyToken(token);
      
      if (!user) {
        return new Response("Unauthorized", { status: 401 });
      }
      
      server.upgrade(req, { data: { user } });
      return;
    }
  },
  websocket: {
    message(ws, message) {
      // Access authenticated user
      const user = ws.data.user;
      // Now safe to process message
    },
  },
});

<severity_table>

常见漏洞摘要

问题查找模式严重性
Shell 注入(函数调用)$(...) 或 $("...")严重
SQL 注入(函数调用)sql(...)严重
SQL 字符串插值SQL 中的 `...${var}...`严重
参数注入以 - 开头的用户输入高
路径遍历Bun.file(userInput)高
命令注入带有用户输入的 Bun.spawn高
CORS 开放Access-Control-Allow-Origin: *中
网络暴露hostname: "0.0.0.0"中
缺少 WebSocket 身份验证server.upgrade 未进行身份验证检查高

</severity_table>

快速审计命令

# Find dangerous shell usage (function call instead of tagged template)
rg '\$\s*\(' . -g "*.ts" -g "*.js"

# Find SQL function calls (should be tagged template)
rg 'sql\s*\(' . -g "*.ts" -g "*.js"

# Find string interpolation in queries
rg '(query|run|exec)\s*\(\s*`' . -g "*.ts" -g "*.js"

# Find Bun.spawn usage
rg 'Bun\.spawn' . -g "*.ts" -g "*.js" -A 2

# Find Bun.file with variables (potential path traversal)
rg 'Bun\.file\s*\([^"'\''`]' . -g "*.ts" -g "*.js"

# Find hostname binding
rg 'hostname.*0\.0\.0\.0' . -g "*.ts" -g "*.js"

# Find CORS headers
rg 'Access-Control-Allow-Origin' . -g "*.ts" -g "*.js"

# Find WebSocket upgrades
rg 'server\.upgrade' . -g "*.ts" -g "*.js" -B 5

加固检查清单

  • 所有 $ Shell 用法都采用标签模板(不使用括号)
  • 所有 sql 用法都采用标签模板(不使用括号)
  • 所有 bun:sqlite 查询都使用参数化
  • 在执行 Shell/spawn 命令前验证用户输入
  • 在适用场景中使用 -- 防止参数注入
  • 使用 basename() 和路径验证清理文件路径
  • 将 CORS 限制为特定来源
  • 开发环境的 hostname 为 127.0.0.1,生产环境明确指定
  • 在升级 WebSocket 连接前进行身份验证
  • 使用 Bun.password.hash 处理密码(而非明文)
  • 检查 bunfig.toml 中是否有可疑设置

评论

支持 Markdown