阿里云主机折上折
  • 微信号
Current Site:Index > CSRF protection implementation solutions

CSRF protection implementation solutions

Author:Chuan Chen 阅读数:27640人阅读 分类: Node.js

Basic Principles of CSRF Attacks

CSRF (Cross-Site Request Forgery) is a common web security threat. Attackers trick users into performing unintended actions on an authenticated web application. For example, after a user logs into a banking website, visiting a malicious site might automatically submit a transfer request to the banking site, exploiting the user's logged-in state to complete unauthorized operations.

Three conditions are required for a successful attack:

  1. The user is logged into the target website.
  2. The user visits a malicious site without logging out.
  3. The target website's interface lacks sufficient protective measures.
// Code that might be included on a malicious site
<img src="http://bank.com/transfer?to=hacker&amount=10000" width="0" height="0">

CSRF Protection Mechanisms in Koa2

The Koa2 ecosystem offers multiple CSRF protection solutions, primarily implemented through the following methods:

  1. Synchronizer Token Pattern: The server generates a token embedded in forms and validates it upon submission.
  2. Double Cookie Verification: The client stores a token and includes it in requests.
  3. SameSite Cookie Attribute: Restricts the use of third-party cookies.

The most commonly used solution is the koa-csrf middleware, which implements the synchronizer token pattern:

const CSRF = require('koa-csrf')
const session = require('koa-session')

app.keys = ['some secret key']
app.use(session(app))
app.use(new CSRF())

Implementing Synchronizer Token Protection

The Synchronizer Token Pattern is a classic CSRF protection solution. The server generates a unique token stored in the session and sends it to the client. The client must include this token when submitting forms, and the server verifies if the token matches.

Steps to implement in Koa2:

  1. Install dependencies:
npm install koa-session koa-csrf
  1. Configure middleware:
const Koa = require('koa')
const session = require('koa-session')
const CSRF = require('koa-csrf')

const app = new Koa()
app.keys = ['your-session-secret']
app.use(session(app))

// Configure CSRF
app.use(new CSRF({
  invalidTokenMessage: 'Invalid CSRF token',
  invalidTokenStatusCode: 403,
  excludedMethods: ['GET', 'HEAD', 'OPTIONS'],
  disableQuery: false
}))
  1. Retrieve the token in routes:
router.get('/form', async (ctx) => {
  await ctx.render('form', {
    csrf: ctx.csrf
  })
})
  1. Include the token in forms:
<form action="/submit" method="POST">
  <input type="hidden" name="_csrf" value="<%= csrf %>">
  <!-- Other form fields -->
  <button type="submit">Submit</button>
</form>

Double Cookie Verification Solution

For API interfaces or frontend-backend separation scenarios, double cookie verification can be used:

  1. Set an HttpOnly authentication cookie upon successful login.
  2. Simultaneously set a non-HttpOnly CSRF token cookie.
  3. The frontend reads the CSRF token from the cookie and adds it to the request headers.
  4. The server compares the token in the request headers with the one in the cookie.

Implementation code:

// Set CSRF Token Cookie
app.use(async (ctx, next) => {
  if (ctx.path === '/login' && ctx.method === 'POST') {
    const token = generateToken()
    ctx.cookies.set('csrf-token', token, { 
      httpOnly: false,
      sameSite: 'strict'
    })
  }
  await next()
})

// Verification middleware
app.use(async (ctx, next) => {
  if (['POST', 'PUT', 'DELETE'].includes(ctx.method)) {
    const cookieToken = ctx.cookies.get('csrf-token')
    const headerToken = ctx.get('x-csrf-token')
    
    if (!cookieToken || cookieToken !== headerToken) {
      ctx.throw(403, 'Invalid CSRF Token')
    }
  }
  await next()
})

Frontend implementation:

// Get CSRF Token
function getCSRFToken() {
  return document.cookie.replace(/(?:(?:^|.*;\s*)csrf-token\s*\=\s*([^;]*).*$)|^.*$/, "$1")
}

// Add header when sending requests
fetch('/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCSRFToken()
  },
  body: JSON.stringify({...})
})

SameSite Cookie Attribute

Modern browsers support the SameSite cookie attribute, which effectively prevents CSRF attacks:

  • Strict: Completely prohibits third-party cookies.
  • Lax: Loose mode, allows some secure requests to carry cookies.
  • None: Disables SameSite protection.

Configuration in Koa2:

ctx.cookies.set('sessionId', '12345', {
  sameSite: 'strict',
  httpOnly: true,
  secure: true
})

Custom CSRF Protection Middleware

For more flexible solutions, custom middleware can be created:

const crypto = require('crypto')

app.use(async (ctx, next) => {
  // Generate token
  ctx.generateCSRFToken = () => {
    const token = crypto.randomBytes(32).toString('hex')
    ctx.session.csrfToken = token
    return token
  }
  
  // Verify token
  ctx.verifyCSRFToken = (token) => {
    return ctx.session.csrfToken && 
           ctx.session.csrfToken === token
  }
  
  await next()
})

// Usage example
router.post('/submit', async (ctx) => {
  const { _csrf } = ctx.request.body
  if (!ctx.verifyCSRFToken(_csrf)) {
    ctx.throw(403, 'Invalid CSRF token')
  }
  // Process legitimate requests
})

CSRF Protection for AJAX Requests

Single-page applications (SPAs) require special handling:

  1. The server embeds the CSRF token in the initial HTML:
<meta name="csrf-token" content="<%= csrfToken %>">
  1. The frontend globally sets AJAX request headers:
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken
  1. Server verification middleware:
app.use(async (ctx, next) => {
  if (ctx.is('application/json')) {
    const token = ctx.get('x-csrf-token')
    if (!token || token !== ctx.session.csrfToken) {
      ctx.throw(403, 'Invalid CSRF token')
    }
  }
  await next()
})

Security Enhancement Measures

Beyond basic protection, the following measures can be taken:

  1. Verify Origin/Referer Headers:
app.use(async (ctx, next) => {
  const origin = ctx.get('origin')
  const referer = ctx.get('referer')
  
  if (['POST', 'PUT', 'DELETE'].includes(ctx.method)) {
    if (origin && !origin.match(/^https?:\/\/(www\.)?yourdomain\.com$/)) {
      ctx.throw(403, 'Request origin not allowed')
    }
  }
  
  await next()
})
  1. Require Re-authentication for Critical Operations:
router.post('/transfer', async (ctx) => {
  const { amount, password } = ctx.request.body
  
  if (!verifyPassword(ctx.state.user.id, password)) {
    ctx.throw(403, 'Password verification failed')
  }
  
  // Execute transfer operation
})
  1. Limit Sensitive Operation Frequency:
const rateLimit = require('koa-ratelimit')

const transferLimiter = rateLimit({
  driver: 'memory',
  db: new Map(),
  duration: 60000,
  max: 3,
  id: (ctx) => ctx.state.user.id
})

router.post('/transfer', transferLimiter, async (ctx) => {
  // Transfer logic
})

Testing CSRF Protection

Ensure protective measures are effective by testing:

  1. Use Postman to simulate cross-origin requests.
  2. Attempt to send requests without a CSRF token.
  3. Test if token reuse is rejected.
  4. Verify the SameSite attribute of cookies.

Example test script:

const request = require('supertest')
const app = require('../app')

describe('CSRF Protection', () => {
  it('should reject request without CSRF token', async () => {
    const res = await request(app)
      .post('/submit')
      .send({ data: 'test' })
    expect(res.status).toEqual(403)
  })
  
  it('should accept request with valid CSRF token', async () => {
    const agent = request.agent(app)
    const getRes = await agent.get('/form')
    const csrfToken = extractTokenFromResponse(getRes)
    
    const postRes = await agent
      .post('/submit')
      .send({ _csrf: csrfToken, data: 'test' })
    expect(postRes.status).toEqual(200)
  })
})

Performance Optimization Considerations

CSRF protection may impact performance. Optimization suggestions:

  1. Exclude routes that don't need protection:
app.use(new CSRF({
  excludedPaths: [/^\/api\/public/, /^\/healthcheck/]
}))
  1. Cache CSRF Tokens:
const tokenCache = new LRU({ max: 1000 })

app.use(async (ctx, next) => {
  if (ctx.method === 'GET' && ctx.path === '/csrf-token') {
    const cachedToken = tokenCache.get(ctx.session.id)
    if (cachedToken) {
      ctx.body = { token: cachedToken }
      return
    }
    // Generate new token
  }
  await next()
})
  1. Use Faster Token Generation Algorithms:
function generateToken() {
  return crypto.randomBytes(16).toString('hex') // Faster than 32 bytes
}

Integration with Other Security Measures

CSRF protection should work in conjunction with other security measures:

  1. CSP Policy:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
  1. XSS Protection:
app.use(require('koa-helmet')())
  1. Enforce HTTPS:
app.use(async (ctx, next) => {
  if (ctx.protocol !== 'https' && process.env.NODE_ENV === 'production') {
    ctx.redirect(`https://${ctx.host}${ctx.url}`)
    return
  }
  await next()
})

Common Issues and Solutions

Issue 1: AJAX requests cannot retrieve CSRF tokens.

Solution: Ensure tokens are exposed to the frontend via meta tags or initial response bodies.

Issue 2: Multiple tabs cause token invalidation.

Solution: Generate new tokens on each page load while allowing old tokens to remain valid for a short period.

app.use(new CSRF({
  tokenLifecycle: 3600000 // 1-hour validity
}))

Issue 3: Mobile API clients cannot handle CSRF.

Solution: Disable CSRF for specific routes and use alternative authentication methods like OAuth.

app.use(new CSRF({
  excludedPaths: [/^\/mobile-api/]
}))

Logging and Monitoring

Logging CSRF verification failures helps detect attack attempts:

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    if (err.status === 403 && err.message.includes('CSRF')) {
      logCSRFAttempt(ctx.ip, ctx.path)
    }
    throw err
  }
})

function logCSRFAttempt(ip, path) {
  console.warn(`CSRF attempt from ${ip} at ${new Date().toISOString()} on ${path}`)
  // Integrate with security monitoring systems
}

Best Practices for Framework Integration

Integration with common Koa2 frameworks:

  1. Integration with Koa-Router:
router.get('/protected', csrfMiddleware, async (ctx) => {
  ctx.body = { token: ctx.csrf }
})

router.post('/protected', csrfMiddleware, async (ctx) => {
  // Automatically verify CSRF
})
  1. Integration with Passport:
app.use(passport.initialize())
app.use(passport.session())
app.use(new CSRF()) // CSRF after authentication
  1. Integration with GraphQL:
const { ApolloServer } = require('apollo-server-koa')

const server = new ApolloServer({
  context: ({ ctx }) => {
    return {
      csrfToken: ctx.csrf
    }
  }
})

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.