Review Bun runtime security audit patterns. Use for auditing Bun-specific vulnerabilities including shell injection, SQL injection, server security, and process spawning. Use proactively when reviewing Bun apps (bun.lockb, bunfig.toml, or bun:* imports present). Examples: - user: "Review this Bun shell script" → audit `$` usage and argument injection - user: "Check my bun:sqlite queries" → verify `sql` tagged template usage - user: "Audit my Bun.serve() setup" → check path traversal and request limits - user: "Is my Bun.spawn() usage safe?" → audit command injection and input validation - user: "Review WebSocket security in Bun" → check authentication before upgrade
Security audit patterns for Bun runtime applications covering shell injection, SQL injection, server security, and Bun-specific vulnerabilities.
</overview> <rules>Bun's shell $ is a tagged template that escapes by default. If you bypass escaping (via raw mode), user input can become command injection.
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) - MUST NOT do this
await $`bash -c "echo ${userInput}"`;
// The nested shell interprets user input as code
Even the safe tagged template is vulnerable to argument injection:
import { $ } from "bun";
// HIGH: Argument injection via -- prefix - MUST validate
const userRepo = "--upload-pack=id>/tmp/pwned";
await $`git ls-remote ${userRepo} main`;
// The -- prefix makes it a command-line argument, not a value
// MUST validate input format before use
const userRepo = getUserInput();
if (!userRepo.match(/^https?:\/\//)) {
throw new Error("Invalid repository URL");
}
await $`git ls-remote ${userRepo} main`;
// MAY use -- to end argument parsing
await $`git ls-remote -- ${userRepo} main`;
sql is a tagged template that parameterizes values. If you build SQL strings manually, you can still be vulnerable.
import { sql } from "bun";
const userId = "1 OR 1=1";
// CRITICAL: Function call - SQL injection! - MUST NOT do this
await sql(`SELECT * FROM users WHERE id = ${userId}`);
// Executes: SELECT * FROM users WHERE id = 1 OR 1=1
// SAFE: Tagged template - parameterized query - MUST do this
await sql`SELECT * FROM users WHERE id = ${userId}`;
// Executes: SELECT * FROM users WHERE id = $1 with params ['1 OR 1=1']
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
const userInput = "'; DROP TABLE users; --";
// CRITICAL: String interpolation - MUST NOT do this
db.run(`INSERT INTO logs VALUES ('${userInput}')`);
// SAFE: Parameterized with .run() - MUST do this
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>
// No input validation - MUST NOT do this
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!
},
});
// MUST 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));
// MUST verify path is within uploads directory
if (!filePath.startsWith(uploadsDir)) {
return new Response("Forbidden", { status: 403 });
}
return new Response(Bun.file(filePath));
},
});
// No body size limit (large uploads can exhaust memory) - SHOULD NOT do this
Bun.serve({
fetch(req) {
return new Response("ok");
},
});
// SHOULD set a max request body size
Bun.serve({
maxRequestBodySize: 1_000_000, // 1 MB
fetch(req) {
return new Response("ok");
},
});
// Wide open CORS - MUST NOT do this
Bun.serve({
fetch(req) {
return new Response("data", {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true", // Dangerous combo!
},
});
},
});
// MUST use 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) - SHOULD verify intent
Bun.serve({
hostname: "0.0.0.0", // Accessible from any network interface
port: 3000,
fetch(req) { /* ... */ },
});
// SHOULD use localhost only for development
Bun.serve({
hostname: "127.0.0.1", // Only local access
port: 3000,
fetch(req) { /* ... */ },
});
// CRITICAL: User input in command array (can still be dangerous) - MUST validate
const filename = userInput; // Could be "--version" or other flags
Bun.spawn(["convert", filename, "output.png"]);
// CRITICAL: Shell execution with user input - MUST NOT do this
Bun.spawn(["sh", "-c", `convert ${userInput} output.png`]);
// MUST 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"]);
// SHOULD use -- to prevent flag injection
Bun.spawn(["convert", "--", filename, "output.png"]);
// HIGH: Path traversal - MUST NOT do this
const userFile = req.query.file; // "../../etc/passwd"
const content = await Bun.file(`./uploads/${userFile}`).text();
// HIGH: Writing to arbitrary paths - MUST NOT do this
await Bun.write(`./data/${userFile}`, content);
// MUST 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.hash is secure by default (uses argon2)
const hash = await Bun.password.hash(password);
// Verify passwords
const isValid = await Bun.password.verify(password, hash);
// MUST check: is it actually being used?
// Common vibecoding mistake: storing plaintext anyway
// MUST NOT store plaintext
db.run("INSERT INTO users (password) VALUES (?)", [password]);
// MUST store 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 - MUST check what gets bundled
// If using Bun with a bundler, verify bundle contents
// SHOULD access server-only
const apiKey = Bun.env.API_KEY;
if (!apiKey) {
throw new Error("API_KEY not configured");
}
// MUST check bunfig.toml for any exposed variables
# MUST check for suspicious configurations
[install]
# Disabling lockfile = supply chain risk - SHOULD NOT do this
save-lockfile = false
# Allowing arbitrary registries - MUST verify trusted
registry = "http://malicious-registry.com"
[run]
# Disabling sandbox (if applicable) - MUST verify intent
Bun.serve({
fetch(req, server) {
if (req.headers.get("upgrade") === "websocket") {
// No auth check before upgrade - MUST NOT do this
server.upgrade(req);
return;
}
},
websocket: {
message(ws, message) {
// Broadcasting without auth - MUST NOT do this
ws.publish("chat", message);
},
},
});
// MUST 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>
| Issue | Pattern to Find | Severity |
|-------|-----------------|----------|
| Shell injection (function call) | $(...) or $("...") | CRITICAL |
| SQL injection (function call) | sql(...) | CRITICAL |
| SQL string interpolation | `...${var}...` in SQL | CRITICAL |
| Argument injection | User 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># 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
</commands>
<checklist>
$ shell usage MUST be tagged template (no parentheses)sql usage MUST be tagged template (no parentheses)-- SHOULD be used to prevent argument injection where applicablenpx skills add justinlevinedotme/security-bun下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer