security-bun
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
How do I install this agent skill?
npx skills add https://github.com/igorwarzocha/opencode-workflows --skill security-bunIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides comprehensive security audit patterns for Bun runtime applications and an automated scanning script. No security issues were detected.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
- Runlayerwarn
2/2 files flagged
What does this agent skill do?
Security audit patterns for Bun runtime applications covering shell injection, SQL injection, server security, and Bun-specific vulnerabilities.
</overview> <rules>The #1 Bun Footgun: Shell Escaping vs Raw 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.
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
Argument Injection (Even with Escaping)
Even the safe tagged template is vulnerable to argument injection:
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 Injection
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!
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 Database Class
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>
Bun.serve() Security
Missing Request Validation
// ❌ 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));
},
});
Request Size Limits (DoS Protection)
// ❌ 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 Configuration
// ❌ 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 });
},
});
Host Binding
// ❌ 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() Command Injection
// ❌ 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() and Bun.write() Path Traversal
// ❌ 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 (Secure, but check usage)
// ✓ 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]);
Environment Variables
// 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 Security
# 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 Security
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>
Common Vulnerabilities Summary
| 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>Quick Audit 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>
Hardening Checklist
- All
$shell usage is tagged template (no parentheses) - All
sqlusage 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
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/igorwarzocha/opencode-workflows/security-bun">View security-bun on skillZs</a>