agents/security-reviewer/.opencode/skill/security-bun/SKILL.md
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
npx skillsauth add igorwarzocha/opencode-workflows security-bunInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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)
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
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`;
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']
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>
// ❌ 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));
},
});
// ❌ 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");
},
});
// ❌ 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) { /* ... */ },
});
// ❌ 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"]);
// ❌ 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.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
# 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)
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>
| 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 is tagged template (no parentheses)sql usage is tagged template (no parentheses)-- used to prevent argument injection where applicabledevelopment
Handle structured co-authoring of professional documentation. Use for proposals, technical specs, and RFCs. Use proactively when a collaborative drafting process (Gathering -> Refinement -> Testing) is needed. Examples: - user: "Draft a technical RFC for the new API" -> follow Stage 1 context gathering - user: "Refine the introduction of this proposal" -> use iterative surgical edits - user: "Test if this document is clear for readers" -> run reader testing workflow
development
Handle Word document (.docx) creation, editing, and analysis with high-fidelity visual review. Use for professional reports, legal documents, and tracked changes. Use proactively when quality and precise formatting are critical. Examples: - user: "Create a professional report in Word" -> use python-docx with render loops - user: "Draft a legal contract with redlines" -> use ooxml redlining workflow - user: "Extract text from this DOCX while preserving structure" -> use pandoc markdown conversion
testing
Apply professional visual themes to documents and presentations. Use for styling artifacts with consistent color palettes and font pairings. Use proactively to quickly improve the aesthetic quality of deliverables. Examples: - user: "Apply a modern theme to this deck" -> use Modern Minimalist theme - user: "I want a tech aesthetic for this doc" -> apply Tech Innovation theme - user: "Create a custom theme for my project" -> generate new color/font specification
tools
Guide for creating effective opencode skills. Use for creating or updating skills that extend agent capabilities with specialized knowledge, workflows, or tool integrations. Examples: - user: "Create a skill for git workflows" → define SKILL.md with instructions and examples - user: "Add examples to my skill" → follow the user: "query" → action pattern - user: "Update skill description" → use literal block scalar and trigger contexts - user: "Structure a complex skill" → organize with scripts/ and references/ directories - user: "Validate my skill" → check structure, frontmatter, and discovery triggers