关于 security-bun
面向 Bun 应用开发者的安全审计技能,涵盖命令与 SQL 注入、路径遍历、服务配置及 Bun 特有风险。包含快速搜索命令和加固清单。
主要功能
- 01检查 Shell 与命令注入风险
- 02识别 SQL 拼接和参数化问题
- 03审查请求校验、CORS 与主机绑定
- 04检查路径遍历和 WebSocket 认证
- 05审查环境变量与 bunfig.toml
- 06提供审计命令和加固清单
适用场景
- 01审计 Bun.shell 和 Bun.spawn 的用户输入处理
- 02检查 bun:sqlite 查询是否存在注入风险
- 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