security-bun

Security audit patterns for Bun runtime applications covering shell injection, SQL injection, server security, and Bun-specific vulnerabilities.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "security-bun" with this command: npx skills add igorwarzocha/opencode-workflows/igorwarzocha-opencode-workflows-security-bun

Security audit patterns for Bun runtime applications covering shell injection, SQL injection, server security, and Bun-specific vulnerabilities.

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 sqlSELECT * 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);

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 &#x26;&#x26; 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 }, }, });

<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>

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

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

security-nextjs

No summary provided by upstream source.

Repository SourceNeeds Review
Security

security-fastapi

No summary provided by upstream source.

Repository SourceNeeds Review
Security

security-express

No summary provided by upstream source.

Repository SourceNeeds Review
Security

security-docker

No summary provided by upstream source.

Repository SourceNeeds Review