10Network

Protocols

Protocols Security — Instruction 10

Coverage

GraphQL, WebSocket, Webhooks, Server-Sent Events (SSE) OWASP API Security Top 10


GraphQL Security

1. Disable Introspection in Production

// 🔴 Introspection in production = complete API map for attackers
// Default: enabled

// 🟢 Disable in production
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  playground: process.env.NODE_ENV !== 'production'
})

2. Query Depth Limiting

// 🔴 Deeply nested query = DoS attack
query {
  user { friends { friends { friends { friends { id name } } } } }
}

// 🟢 Limit depth
import depthLimit from 'graphql-depth-limit'
const server = new ApolloServer({
  validationRules: [depthLimit(5)]
})

3. Query Complexity Limiting

// 🔴 Complex query with many fields = DoS
// 🟢 Limit complexity score
import { createComplexityLimitRule } from 'graphql-validation-complexity'
const server = new ApolloServer({
  validationRules: [createComplexityLimitRule(1000)]
})

4. Field-Level Authorization

// 🔴 Resolver without auth check
const resolvers = {
  Query: {
    adminUsers: () => User.findAll()  // no auth!
  }
}

// 🟢 Check auth in every sensitive resolver
const resolvers = {
  Query: {
    adminUsers: (_, __, { user }) => {
      if (!user || user.role !== 'admin') throw new ForbiddenError('Access denied')
      return User.findAll()
    }
  }
}

5. GraphQL Error Masking

// 🔴 Internal errors exposed to clients
// ApolloServer default shows full error in development

// 🟢 Mask errors in production
const server = new ApolloServer({
  formatError: (error) => {
    if (process.env.NODE_ENV === 'production') {
      return new Error('Internal server error')
    }
    return error
  }
})

6. GraphQL Batching Attacks

// 🔴 Batched queries bypass rate limiting
[{ query: "{ user(id: 1) { id } }" }, ... x 1000]

// 🟢 Limit batch size
// apollo-server-express: batchingEnabled (disable or limit)
const server = new ApolloServer({ allowBatchedHttpRequests: false })

WebSocket Security

7. WSS (WebSocket Secure) Only

// 🔴 Unencrypted WebSocket in production
const ws = new WebSocket('ws://api.example.com')

// 🟢 Always WSS in production
const ws = new WebSocket('wss://api.example.com')

8. Origin Validation on Server

// 🔴 Accepts WebSocket from any origin
// 🟢 Validate origin on upgrade
const wss = new WebSocket.Server({
  server,
  verifyClient: ({ origin, req }) => {
    const allowed = ['https://app.yourdomain.com']
    return allowed.includes(origin)
  }
})

9. Authentication per WebSocket Connection

// 🔴 No auth = anyone can connect
wss.on('connection', (ws) => {
  ws.on('message', handleMessage)
})

// 🟢 Authenticate on connection
wss.on('connection', (ws, req) => {
  const token = new URL(req.url, 'http://localhost').searchParams.get('token')
  const user = verifyToken(token)
  if (!user) return ws.close(1008, 'Unauthorized')
  ws.userId = user.id
})

10. Message Validation

// 🔴 Trust WebSocket message content blindly
ws.on('message', (data) => {
  const msg = JSON.parse(data)
  db.query(msg.sql)  // 🔴 CRITICAL
})

// 🟢 Validate and sanitize every message
ws.on('message', (data) => {
  let msg
  try { msg = JSON.parse(data) } catch { return ws.close(1003, 'Invalid data') }
  if (!isValidMessageSchema(msg)) return ws.close(1003, 'Invalid schema')
  handleValidMessage(msg)
})

11. WebSocket Rate Limiting

// Track messages per connection, disconnect abusers
const msgCount = new Map()
ws.on('message', (data) => {
  const count = (msgCount.get(ws) || 0) + 1
  msgCount.set(ws, count)
  if (count > 100) return ws.terminate()  // too many messages
})

12. WebSocket Timeout

// Disconnect idle connections
const TIMEOUT = 5 * 60 * 1000  // 5 minutes
let pingTimeout
ws.on('pong', () => clearTimeout(pingTimeout))
const interval = setInterval(() => {
  if (ws.readyState === ws.OPEN) {
    ws.ping()
    pingTimeout = setTimeout(() => ws.terminate(), 30000)
  }
}, 30000)
ws.on('close', () => clearInterval(interval))

Webhook Security

13. HMAC Signature Verification

// 🔴 No signature verification = anyone can trigger webhooks
app.post('/webhook', (req, res) => {
  processWebhook(req.body)
})

// 🟢 Always verify HMAC signature
import { createHmac, timingSafeEqual } from 'crypto'
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature']
  const expected = createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex')
  if (!timingSafeEqual(Buffer.from(signature), Buffer.from(`sha256=${expected}`))) {
    return res.status(401).json({ error: 'Invalid signature' })
  }
  processWebhook(JSON.parse(req.body))
})

14. Webhook Idempotency

// 🔴 Processing duplicate webhooks = double charges, duplicate emails
// 🟢 Track and deduplicate
const processed = new Set()  // Use Redis in production
app.post('/webhook', (req, res) => {
  const eventId = req.headers['x-webhook-id']
  if (processed.has(eventId)) return res.status(200).json({ ok: true })
  processed.add(eventId)
  processWebhook(req.body)
})

Server-Sent Events (SSE)

15. SSE Authentication

// 🔴 SSE without auth = anyone can subscribe to events
app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream')
  sendSensitiveData(res)  // 🔴
})

// 🟢 Require auth
app.get('/events', requireAuth, (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')
  res.setHeader('Connection', 'keep-alive')
  sendUserSpecificEvents(res, req.user.id)
})

16. SSE Reconnection Limit

// 🔴 Unlimited reconnections = DoS vector
// 🟢 Track and limit connections per user
const connections = new Map()
app.get('/events', requireAuth, (req, res) => {
  const userId = req.user.id
  if ((connections.get(userId) || 0) >= 3) {
    return res.status(429).json({ error: 'Too many connections' })
  }
  connections.set(userId, (connections.get(userId) || 0) + 1)
  req.on('close', () => connections.set(userId, connections.get(userId) - 1))
})