security-bun

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
<overview>
Security audit patterns for Bun runtime applications covering shell injection, SQL injection, server security, and Bun-specific vulnerabilities.
</overview> <rules>
<overview>
针对Bun运行时应用的安全审计模式,涵盖Shell注入、SQL注入、服务器安全以及Bun特有的漏洞。
</overview> <rules>

The #1 Bun Footgun: Shell Escaping vs Raw Shell

Bun头号陷阱:Shell转义 vs 原生Shell

Bun's shell
$
is a tagged template that escapes by default. If you bypass escaping (via raw mode), user input can become command injection.
typescript
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
Bun的Shell
$
是默认会转义的标签模板。如果绕过转义(通过原生模式),用户输入可能会导致命令注入。
typescript
import { $ } from "bun";

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

// ✓ 安全:Tagged template - 自动转义
await $`echo ${userInput}`;
// 执行:echo 'hello; rm -rf /'

// ❌ 严重:生成新Shell(绕过Bun转义)
await $`bash -c "echo ${userInput}"`;
// 嵌套Shell会将用户输入解析为代码

Argument Injection (Even with Escaping)

参数注入(即使使用转义)

Even the safe tagged template is vulnerable to argument injection:
typescript
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`;
即使是安全的标签模板也可能存在参数注入漏洞:
typescript
import { $ } from "bun";

// ❌ 高危:通过--前缀进行参数注入
const userRepo = "--upload-pack=id>/tmp/pwned";
await $`git ls-remote ${userRepo} main`;
// --前缀会将其视为命令行参数,而非值

// ✓ 使用前验证输入格式
const userRepo = getUserInput();
if (!userRepo.match(/^https?:\/\//)) {
  throw new Error("无效的仓库URL");
}
await $`git ls-remote ${userRepo} main`;

// ✓ 或者使用--结束参数解析
await $`git ls-remote -- ${userRepo} main`;

bun:sqlite SQL Injection

bun:sqlite SQL注入

sql
is a tagged template that parameterizes values. If you build SQL strings manually, you can still be vulnerable.
typescript
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']
sql
是会参数化值的标签模板。如果手动构建SQL字符串,仍然可能存在漏洞。
typescript
import { sql } from "bun";

const userId = "1 OR 1=1";

// ❌ 严重:函数调用 - SQL注入!
await sql(`SELECT * FROM users WHERE id = ${userId}`);
// 执行:SELECT * FROM users WHERE id = 1 OR 1=1

// ✓ 安全:Tagged template - 参数化查询
await sql`SELECT * FROM users WHERE id = ${userId}`;
// 执行:SELECT * FROM users WHERE id = $1,参数为 ['1 OR 1=1']

bun:sqlite Database Class

bun:sqlite Database类

typescript
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);
</rules> <vulnerabilities>
typescript
import { Database } from "bun:sqlite";

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

// ❌ 严重:字符串插值
db.run(`INSERT INTO logs VALUES ('${userInput}')`);

// ✓ 安全:使用.run()进行参数化
db.run("INSERT INTO logs VALUES (?)", [userInput]);

// ✓ 安全:预编译语句
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
stmt.get(userInput);

// ✓ 安全:带参数的查询
db.query("SELECT * FROM users WHERE email = ?").get(userInput);
</rules> <vulnerabilities>

Bun.serve() Security

Bun.serve()安全

Missing Request Validation

缺少请求验证

typescript
// ❌ 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));
  },
});
typescript
// ❌ 无输入验证
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    const file = url.searchParams.get("file");
    return new Response(Bun.file(`./uploads/${file}`)); // 路径遍历!
  },
});

// ✓ 验证并清理
import { join, basename, resolve } from "path";

Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    const file = url.searchParams.get("file");
    
    // 清理文件名
    const safeName = basename(file ?? "");
    const uploadsDir = resolve("./uploads");
    const filePath = resolve(join(uploadsDir, safeName));
    
    // 验证路径是否在uploads目录内
    if (!filePath.startsWith(uploadsDir)) {
      return new Response("禁止访问", { status: 403 });
    }
    
    return new Response(Bun.file(filePath));
  },
});

Request Size Limits (DoS Protection)

请求大小限制(DoS防护)

typescript
// ❌ 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");
  },
});
typescript
// ❌ 无请求体大小限制(大上传会耗尽内存)
Bun.serve({
  fetch(req) {
    return new Response("ok");
  },
});

// ✓ 设置最大请求体大小
Bun.serve({
  maxRequestBodySize: 1_000_000, // 1 MB
  fetch(req) {
    return new Response("ok");
  },
});

CORS Configuration

CORS配置

typescript
// ❌ 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 });
  },
});
typescript
// ❌ 完全开放的CORS
Bun.serve({
  fetch(req) {
    return new Response("data", {
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Credentials": "true", // 危险组合!
      },
    });
  },
});

// ✓ 明确的源白名单
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 });
  },
});

Host Binding

主机绑定

typescript
// ❌ 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) { /* ... */ },
});
typescript
// ❌ 暴露到网络(有时是无意的)
Bun.serve({
  hostname: "0.0.0.0", // 可从任何网络接口访问
  port: 3000,
  fetch(req) { /* ... */ },
});

// ✓ 开发环境仅本地访问
Bun.serve({
  hostname: "127.0.0.1", // 仅本地可访问
  port: 3000,
  fetch(req) { /* ... */ },
});

Bun.spawn() Command Injection

Bun.spawn()命令注入

typescript
// ❌ 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"]);
typescript
// ❌ 严重:命令数组中包含用户输入(仍可能危险)
const filename = userInput; // 可能是"--version"或其他标志
Bun.spawn(["convert", filename, "output.png"]);

// ❌ 严重:带用户输入的Shell执行
Bun.spawn(["sh", "-c", `convert ${userInput} output.png`]);

// ✓ 先验证输入
const filename = userInput;
if (!filename.match(/^[a-zA-Z0-9_-]+\.(jpg|png|gif)$/)) {
  throw new Error("无效文件名");
}
Bun.spawn(["convert", filename, "output.png"]);

// ✓ 使用--防止标志注入
Bun.spawn(["convert", "--", filename, "output.png"]);

Bun.file() and Bun.write() Path Traversal

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

typescript
// ❌ 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();
typescript
// ❌ 高危:路径遍历
const userFile = req.query.file; // "../../etc/passwd"
const content = await Bun.file(`./uploads/${userFile}`).text();

// ❌ 高危:写入任意路径
await Bun.write(`./data/${userFile}`, content);

// ✓ 清理路径
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("无效路径");
  }
  
  return fullPath;
}

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

Bun.password (Secure, but check usage)

Bun.password(安全,但需检查使用方式)

typescript
// ✓ 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]);
typescript
// ✓ Bun.password.hash默认是安全的(使用argon2)
const hash = await Bun.password.hash(password);

// ✓ 验证密码
const isValid = await Bun.password.verify(password, hash);

// ⚠️ 但需检查:是否真的正确使用?
// 常见的编码错误:仍然存储明文

// ❌ 存储明文
db.run("INSERT INTO users (password) VALUES (?)", [password]);

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

Environment Variables

环境变量

typescript
// 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
typescript
// Bun.env与process.env相同

// ❌ 客户端代码中包含密钥
// 如果使用Bun进行打包,检查哪些内容会被打包

// ✓ 仅服务器端访问
const apiKey = Bun.env.API_KEY;
if (!apiKey) {
  throw new Error("未配置API_KEY");
}

// 检查bunfig.toml中的暴露变量

bunfig.toml Security

bunfig.toml安全

toml
undefined
toml
undefined

Check for suspicious configurations

检查可疑配置

[install]
[install]

❌ Disabling lockfile = supply chain risk

❌ 禁用锁文件 = 供应链风险

save-lockfile = false
save-lockfile = false

❌ Allowing arbitrary registries

❌ 允许任意注册表

❌ Disabling sandbox (if applicable)

❌ 禁用沙箱(如果适用)

undefined
undefined

WebSocket Security

WebSocket安全

typescript
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
    },
  },
});
</vulnerabilities>
<severity_table>
typescript
Bun.serve({
  fetch(req, server) {
    if (req.headers.get("upgrade") === "websocket") {
      // ❌ 升级前无身份验证检查
      server.upgrade(req);
      return;
    }
  },
  websocket: {
    message(ws, message) {
      // ❌ 无身份验证的广播
      ws.publish("chat", message);
    },
  },
});

// ✓ 升级前进行身份验证
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("未授权", { status: 401 });
      }
      
      server.upgrade(req, { data: { user } });
      return;
    }
  },
  websocket: {
    message(ws, message) {
      // 访问已认证用户
      const user = ws.data.user;
      // 现在可以安全处理消息
    },
  },
});
</vulnerabilities>
<severity_table>

Common Vulnerabilities Summary

常见漏洞汇总

IssuePattern to FindSeverity
Shell injection (function call)
$(
...
)
or
$("...")
CRITICAL
SQL injection (function call)
sql(
...
)
CRITICAL
SQL string interpolation
`...${var}...`
in SQL
CRITICAL
Argument injectionUser input starting with
-
HIGH
Path traversal
Bun.file(userInput)
HIGH
Command injection
Bun.spawn
with user input
HIGH
Open CORS
Access-Control-Allow-Origin: *
MEDIUM
Network exposure
hostname: "0.0.0.0"
MEDIUM
Missing WebSocket auth
server.upgrade
without auth check
HIGH
</severity_table>
<commands>
问题查找模式严重程度
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>
<commands>

Quick Audit Commands

快速审计命令

bash
undefined
bash
undefined

Find dangerous shell usage (function call instead of tagged template)

查找危险的Shell使用(函数调用而非标签模板)

rg '$\s*(' . -g ".ts" -g ".js"
rg '$\s*(' . -g ".ts" -g ".js"

Find SQL function calls (should be tagged template)

查找SQL函数调用(应为标签模板)

rg 'sql\s*(' . -g ".ts" -g ".js"
rg 'sql\s*(' . -g ".ts" -g ".js"

Find string interpolation in queries

查找查询中的字符串插值

rg '(query|run|exec)\s*(\s*`' . -g ".ts" -g ".js"
rg '(query|run|exec)\s*(\s*`' . -g ".ts" -g ".js"

Find Bun.spawn usage

查找Bun.spawn的使用

rg 'Bun.spawn' . -g ".ts" -g ".js" -A 2
rg 'Bun.spawn' . -g ".ts" -g ".js" -A 2

Find Bun.file with variables (potential path traversal)

查找带变量的Bun.file(潜在路径遍历)

rg 'Bun.file\s*([^"'''`]' . -g ".ts" -g ".js"
rg 'Bun.file\s*([^"'''`]' . -g ".ts" -g ".js"

Find hostname binding

查找主机名绑定

rg 'hostname.0.0.0.0' . -g ".ts" -g "*.js"
rg 'hostname.0.0.0.0' . -g ".ts" -g "*.js"

Find CORS headers

查找CORS头

rg 'Access-Control-Allow-Origin' . -g ".ts" -g ".js"
rg 'Access-Control-Allow-Origin' . -g ".ts" -g ".js"

Find WebSocket upgrades

查找WebSocket升级

rg 'server.upgrade' . -g ".ts" -g ".js" -B 5

</commands>

<checklist>
rg 'server.upgrade' . -g ".ts" -g ".js" -B 5

</commands>

<checklist>

Hardening Checklist

加固检查清单

  • All
    $
    shell usage is tagged template (no parentheses)
  • All
    sql
    usage is tagged template (no parentheses)
  • All bun:sqlite queries use parameterization
  • User input validated before shell/spawn commands
  • --
    used to prevent argument injection where applicable
  • File paths sanitized with basename() and path validation
  • CORS restricted to specific origins
  • hostname is 127.0.0.1 for dev, explicit for prod
  • WebSocket connections authenticated before upgrade
  • Bun.password.hash used for passwords (not plaintext)
  • bunfig.toml reviewed for suspicious settings
</checklist>
  • 所有
    $
    Shell使用均为标签模板(无括号)
  • 所有
    sql
    使用均为标签模板(无括号)
  • 所有bun:sqlite查询均使用参数化
  • Shell/spawn命令使用前验证用户输入
  • 适用时使用--防止参数注入
  • 使用basename()和路径验证清理文件路径
  • CORS限制为特定源
  • 开发环境hostname为127.0.0.1,生产环境明确配置
  • WebSocket连接升级前进行身份验证
  • 使用Bun.password.hash存储密码(而非明文)
  • 审核bunfig.toml中的可疑设置
</checklist>