# 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

- Skill: `jal-co/security-bun` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add jal-co/security-bun`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jal-co/security-bun/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: jal-co (https://skillmd.com/u/jal-co)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jal-co/security-bun

---


<overview>

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.

```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) - MUST NOT do this
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:

```typescript
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`;
```

## bun:sqlite SQL Injection

`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! - 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']
```

### bun:sqlite Database Class

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

## Bun.serve() Security

### Missing Request Validation

```typescript
// 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));
  },
});
```

### Request Size Limits (DoS Protection)

```typescript
// 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");
  },
});
```

### CORS Configuration

```typescript
// 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 });
  },
});
```

### Host Binding

```typescript
// 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) { /* ... */ },
});
```

## Bun.spawn() Command Injection

```typescript
// 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"]);
```

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

```typescript
// 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 (Secure, but check usage)

```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);

// 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]);
```

## Environment Variables

```typescript
// 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
```

### bunfig.toml Security

```toml
# 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
```

## WebSocket Security

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

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

```bash
# 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 MUST be tagged template (no parentheses)
- [ ] All `sql` usage MUST be tagged template (no parentheses)
- [ ] All bun:sqlite queries MUST use parameterization
- [ ] User input MUST be validated before shell/spawn commands
- [ ] `--` SHOULD be used to prevent argument injection where applicable
- [ ] File paths MUST be sanitized with basename() and path validation
- [ ] CORS MUST be restricted to specific origins
- [ ] hostname SHOULD be 127.0.0.1 for dev, explicit for prod
- [ ] WebSocket connections MUST be authenticated before upgrade
- [ ] Bun.password.hash MUST be used for passwords (not plaintext)
- [ ] bunfig.toml SHOULD be reviewed for suspicious settings

</checklist>

