
Security Express
- 126 installs
- 125 repo stars
- Updated February 4, 2026
- igorwarzocha/opencode-workflows
Audit Express.js apps for missing Helmet headers, weak CORS, unlimited body-parser, auth middleware order, and path traversal.
About
A security-audit skill for Express.js middleware and routes. A developer uses it to add Helmet, verify CORS allowlists, check auth middleware coverage, and secure session cookies.
- Checks Helmet, x-powered-by, and CORS origin/credentials config
- Covers auth middleware route order, path traversal, and cookie flags
Security Express by the numbers
- 126 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #932 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/igorwarzocha/opencode-workflows --skill security-expressAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 126 |
|---|---|
| repo stars | ★ 125 |
| Last updated | February 4, 2026 |
| Repository | igorwarzocha/opencode-workflows ↗ |
What it does
Audit Express.js apps for missing Helmet headers, weak CORS, unlimited body-parser, auth middleware order, and path traversal.
Files
<overview>
Security audit patterns for Express.js applications covering essential security middleware, CORS configuration, auth patterns, and common vulnerabilities.
</overview>
<rules>
Essential Security Middleware
Helmet.js (Security Headers)
// ❌ Missing security headers
const app = express();
// ✓ Use Helmet
const helmet = require('helmet');
app.use(helmet());Check if Helmet is installed and used. It sets:
- Content-Security-Policy
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Strict-Transport-Security
- And more...
Disable X-Powered-By
// ❌ Default (header reveals framework)
const app = express();
// ✓ Disable fingerprinting
app.disable('x-powered-by');
// or: app.set('x-powered-by', false);CORS Configuration
// ❌ CRITICAL: Allow all origins
app.use(cors());
app.use(cors({ origin: '*' }));
// ❌ HIGH: Reflect origin with credentials
app.use(cors({
origin: true, // Reflects any origin!
credentials: true
}));
// ✓ Explicit allowlist
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
}));
// ✓ Function for dynamic validation
app.use(cors({
origin: (origin, callback) => {
const allowed = ['https://app.example.com'];
if (!origin || allowed.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));Body Parser Limits
// ❌ No limit (DoS risk)
app.use(express.json());
// ✓ Set reasonable limits
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: true, limit: '100kb' }));</rules>
<vulnerabilities>
Auth Middleware Patterns
Missing Auth on Routes
// ❌ No auth on admin routes
app.get('/api/admin/users', async (req, res) => {
res.json(await User.find());
});
// ✓ Auth middleware applied
app.get('/api/admin/users', requireAuth, requireAdmin, async (req, res) => {
res.json(await User.find());
});Middleware Order Matters
// ❌ Wrong order - static files before auth
app.use(express.static('uploads')); // Exposed!
app.use(requireAuth);
// ✓ Auth before protected static files
app.use('/public', express.static('public')); // Intentionally public
app.use(requireAuth);
app.use('/uploads', express.static('uploads')); // Now protectedRouter-Level Auth Gaps
// Check: Is auth applied to all routes in admin router?
const adminRouter = express.Router();
adminRouter.use(requireAuth); // Applied to all routes below
adminRouter.get('/users', getUsers);
adminRouter.delete('/users/:id', deleteUser);
// ❌ Watch for routes defined BEFORE the middleware
const apiRouter = express.Router();
apiRouter.get('/health', getHealth); // No auth (intentional?)
apiRouter.use(requireAuth);
apiRouter.get('/users', getUsers); // Has authCommon Vulnerabilities
SQL/NoSQL Injection
// ❌ String interpolation
const user = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// ✓ Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
// ❌ MongoDB injection
const user = await User.findOne({ email: req.body.email }); // If email is { $gt: "" }
// ✓ Validate input type
if (typeof req.body.email !== 'string') return res.status(400).json({ error: 'Invalid email' });Path Traversal
// ❌ User-controlled path
app.get('/files/:filename', (req, res) => {
res.sendFile(`./uploads/${req.params.filename}`); // ../../etc/passwd
});
// ✓ Validate and normalize
const path = require('path');
app.get('/files/:filename', (req, res) => {
const filename = path.basename(req.params.filename);
const filepath = path.join(__dirname, 'uploads', filename);
if (!filepath.startsWith(path.join(__dirname, 'uploads'))) {
return res.status(400).json({ error: 'Invalid path' });
}
res.sendFile(filepath);
});Error Handling
// ❌ Stack traces in production
app.use((err, req, res, next) => {
res.status(500).json({ error: err.stack }); // Leaks internals
});
// ✓ Safe error handler
app.use((err, req, res, next) => {
console.error(err); // Log for debugging
res.status(500).json({ error: 'Internal server error' });
});Session Security
// ❌ Insecure session config
app.use(session({
secret: 'keyboard cat', // Hardcoded!
cookie: { secure: false }, // No HTTPS requirement
}));
// ✓ Secure config
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JS access
sameSite: 'strict', // CSRF protection
maxAge: 1000 * 60 * 60 * 24, // 24 hours
},
}));Rate Limiting
// Check for rate limiting on auth routes
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts',
});
app.post('/api/login', authLimiter, loginHandler);
app.post('/api/register', authLimiter, registerHandler);
app.post('/api/forgot-password', authLimiter, forgotPasswordHandler);</vulnerabilities>
<commands>
Quick Audit Commands
# Check if Helmet is used
rg -n 'helmet\\(' . -g "*.js" -g "*.ts"
# Check if x-powered-by is disabled
rg -n "x-powered-by" . -g "*.js" -g "*.ts"# Check for helmet
rg "helmet" package.json
rg "require\\(['\"]helmet" .
rg "from ['\"]helmet" .
# Find CORS config
rg "cors\\(" . -g "*.js" -g "*.ts" -A 5
# Find routes without auth middleware
rg "app\\.(get|post|put|delete|patch)\\(" . -A 1 | grep -v "require.*[Aa]uth"
# Find string interpolation in queries
rg "(query|find|findOne|exec).*\\`" . -g "*.js" -g "*.ts"
# Check session config
rg "session\\(" . -A 10</commands>
<checklist>
Hardening Checklist
- [ ] Helmet.js installed and used
- [ ] CORS restricted to specific origins
- [ ] Body parser has size limits
- [ ] Auth middleware on all protected routes
- [ ] Rate limiting on auth endpoints
- [ ] Session cookies: secure, httpOnly, sameSite
- [ ] No hardcoded secrets
- [ ] Error handler doesn't leak stack traces
- [ ] Input validation on all user input
- [ ] Parameterized queries (no string concat)
</checklist>
#!/usr/bin/env bash
# Express Security Scanner - First-pass automated detection
# Usage: ./scan.sh [directory]
set -euo pipefail
DIR="${1:-.}"
FOUND=0
echo "=== EXPRESS SECURITY SCAN ==="
echo "Directory: $DIR"
echo "Timestamp: $(date -Iseconds)"
echo ""
if ! command -v rg &> /dev/null; then
echo "[ERROR] ripgrep (rg) required"
exit 1
fi
report() {
local severity="$1"
local title="$2"
local details="${3:-}"
echo "[$severity] $title"
[[ -n "$details" ]] && echo " $details"
echo ""
FOUND=$((FOUND + 1))
}
echo "=== CHECKING: Helmet.js ==="
echo ""
if [[ -f "$DIR/package.json" ]]; then
if ! rg -q '"helmet"' "$DIR/package.json" 2>/dev/null; then
report "MEDIUM" "Helmet.js not in dependencies" "Install with: npm install helmet"
else
# Check if it's actually used
if ! rg -q 'helmet\(\)' "$DIR" -g "*.js" -g "*.ts" 2>/dev/null; then
report "MEDIUM" "Helmet.js installed but not used" "Add: app.use(helmet())"
else
echo "[✓] Helmet.js is installed and appears to be used"
fi
fi
fi
echo ""
echo "=== LOW: X-Powered-By Header ==="
echo ""
if ! rg -q "x-powered-by" "$DIR" -g "*.js" -g "*.ts" 2>/dev/null; then
report "LOW" "x-powered-by not explicitly disabled (framework fingerprinting)" "Add: app.disable('x-powered-by')"
else
echo "[✓] x-powered-by appears to be configured"
fi
echo ""
echo "=== CRITICAL: CORS Misconfiguration ==="
echo ""
# Wildcard CORS
while IFS=: read -r file line match; do
[[ -z "$file" ]] && continue
if echo "$match" | grep -qE "origin.*['\"]\*['\"]|origin.*true|cors\(\)"; then
report "HIGH" "Permissive CORS configuration" "$file:$line"
fi
done < <(rg -n --no-heading 'cors\(' "$DIR" -g "*.js" -g "*.ts" -A 3 2>/dev/null || true)
echo "=== HIGH: SQL Injection Patterns ==="
echo ""
# String interpolation in queries
while IFS=: read -r file line match; do
[[ -z "$file" ]] && continue
report "HIGH" "Potential SQL injection (string interpolation in query)" "$file:$line"
done < <(rg -n --no-heading '(query|execute)\s*\(\s*`' "$DIR" -g "*.js" -g "*.ts" 2>/dev/null || true)
# String concatenation in queries
while IFS=: read -r file line match; do
[[ -z "$file" ]] && continue
report "HIGH" "Potential SQL injection (string concatenation)" "$file:$line"
done < <(rg -n --no-heading 'SELECT.*\+.*req\.' "$DIR" -g "*.js" -g "*.ts" 2>/dev/null || true)
echo "=== HIGH: Session Security ==="
echo ""
# Hardcoded session secret
while IFS=: read -r file line match; do
[[ -z "$file" ]] && continue
if echo "$match" | grep -qE "secret.*['\"][^'\"]{5,}['\"]" && ! echo "$match" | grep -qE "process\.env|env\."; then
report "HIGH" "Potential hardcoded session secret" "$file:$line"
fi
done < <(rg -n --no-heading 'session\(' "$DIR" -g "*.js" -g "*.ts" -A 5 2>/dev/null || true)
# Insecure cookie settings
while IFS=: read -r file line match; do
[[ -z "$file" ]] && continue
if echo "$match" | grep -qE "secure.*false"; then
report "MEDIUM" "Session cookie secure:false (not HTTPS-only)" "$file:$line"
fi
done < <(rg -n --no-heading 'cookie.*{' "$DIR" -g "*.js" -g "*.ts" -A 5 2>/dev/null || true)
echo "=== MEDIUM: Rate Limiting ==="
echo ""
if [[ -f "$DIR/package.json" ]]; then
if ! rg -q '"express-rate-limit"' "$DIR/package.json" 2>/dev/null; then
report "MEDIUM" "No rate limiting package found" "Consider: npm install express-rate-limit"
fi
fi
echo "=== MEDIUM: Body Parser Limits ==="
echo ""
# Check for body parser without limits
while IFS=: read -r file line match; do
[[ -z "$file" ]] && continue
if ! echo "$match" | grep -qE "limit"; then
report "LOW" "Body parser without size limit (DoS risk)" "$file:$line"
fi
done < <(rg -n --no-heading 'express\.json\(|express\.urlencoded\(' "$DIR" -g "*.js" -g "*.ts" 2>/dev/null || true)
echo "=== SUMMARY ==="
if [[ $FOUND -gt 0 ]]; then
echo "[!] Found $FOUND potential issues. Review above."
exit 1
else
echo "[✓] No obvious Express security issues detected"
exit 0
fi