Preventing CSRF Attacks in React Applications

Loading

Cross-Site Request Forgery (CSRF) remains a significant threat to web applications. Here’s how to comprehensively protect your React applications against CSRF attacks:

Core Defense Mechanisms

1. Anti-CSRF Tokens (Synchronizer Token Pattern)

Server-Side Implementation (Node.js/Express Example):

// Generate and store CSRF token
app.use((req, res, next) => {
  if (!req.session.csrfToken) {
    req.session.csrfToken = crypto.randomBytes(32).toString('hex');
  }
  next();
});

// Verify CSRF token middleware
const verifyCsrf = (req, res, next) => {
  const clientToken = req.headers['x-csrf-token'] || req.body.csrfToken;
  if (!clientToken || clientToken !== req.session.csrfToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  next();
};

React Integration:

// Custom hook to fetch and manage CSRF token
const useCsrfToken = () => {
  const [csrfToken, setCsrfToken] = useState('');

  useEffect(() => {
    const fetchCsrfToken = async () => {
      const response = await fetch('/api/csrf-token', {
        credentials: 'include' // Important for session cookies
      });
      const data = await response.json();
      setCsrfToken(data.token);
    };

    fetchCsrfToken();
  }, []);

  return csrfToken;
};

// Usage in component
const FormComponent = () => {
  const csrfToken = useCsrfToken();

  const handleSubmit = async (e) => {
    e.preventDefault();
    await fetch('/api/sensitive-action', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
      },
      body: JSON.stringify({ data: 'value' }),
      credentials: 'include'
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="hidden" name="csrfToken" value={csrfToken} />
      {/* Other form fields */}
    </form>
  );
};

Advanced Protection Strategies

2. SameSite Cookie Attribute

Express.js Configuration:

app.use(session({
  secret: 'your-secret-key',
  cookie: {
    sameSite: 'strict', // or 'lax' for GET requests from external sites
    secure: true, // HTTPS only
    httpOnly: true
  }
}));

3. Double Submit Cookie Pattern

Implementation:

// Server middleware to set cookie and verify
app.use((req, res, next) => {
  const cookieToken = req.cookies['X-CSRF-TOKEN'];
  const headerToken = req.headers['x-csrf-token'];

  if (req.method !== 'GET' && cookieToken !== headerToken) {
    return res.status(403).send('CSRF validation failed');
  }

  if (!cookieToken) {
    const token = crypto.randomBytes(32).toString('hex');
    res.cookie('X-CSRF-TOKEN', token, {
      sameSite: 'strict',
      secure: true
    });
  }

  next();
});

// React - automatically include token from cookie in requests
axios.interceptors.request.use(config => {
  const token = document.cookie
    .split('; ')
    .find(row => row.startsWith('X-CSRF-TOKEN='))
    ?.split('=')[1];

  if (token && ['post', 'put', 'patch', 'delete'].includes(config.method)) {
    config.headers['X-CSRF-Token'] = token;
  }

  return config;
});

Framework-Specific Solutions

1. Next.js with CSRF Protection

API Route (Next.js):

// pages/api/csrf.js
export default function handler(req, res) {
  if (req.method === 'GET') {
    const token = crypto.randomBytes(32).toString('hex');
    res.setHeader('Set-Cookie', `csrfToken=${token}; HttpOnly; SameSite=Strict; Secure`);
    return res.status(200).json({ token });
  }
  res.status(405).end();
}

// Custom _app.js to inject CSRF token
function MyApp({ Component, pageProps, csrfToken }) {
  return (
    <>
      <Component {...pageProps} csrfToken={csrfToken} />
      <script
        dangerouslySetInnerHTML={{
          __html: `window.__CSRF_TOKEN__ = "${csrfToken}";`
        }}
      />
    </>
  );
}

MyApp.getInitialProps = async ({ ctx }) => {
  const csrfResponse = await fetch(`${process.env.BASE_URL}/api/csrf`, {
    headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined
  });
  const { token } = await csrfResponse.json();
  return { csrfToken: token };
};

Security Headers for CSRF Protection

1. Content Security Policy (CSP)

<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
               form-action 'self'; 
               frame-ancestors 'none';">

2. Additional Security Headers

// Express.js configuration
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      formAction: ["'self'"], // Prevent form submission to external domains
      frameAncestors: ["'none'"] // Prevent framing attacks
    }
  },
  referrerPolicy: { policy: 'same-origin' }
}));

Real-World Implementation Patterns

1. Protected API Client

// apiClient.js
const apiClient = axios.create({
  baseURL: process.env.API_BASE_URL,
  withCredentials: true
});

// Add CSRF token to all mutating requests
apiClient.interceptors.request.use(config => {
  if (['post', 'put', 'patch', 'delete'].includes(config.method)) {
    const csrfToken = getCsrfToken(); // From cookie or state
    config.headers['X-CSRF-Token'] = csrfToken;
  }
  return config;
});

// Handle CSRF token errors
apiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 403 && 
        error.response.data?.error === 'Invalid CSRF token') {
      // Trigger token refresh and retry
      return handleCsrfError(error);
    }
    return Promise.reject(error);
  }
);

2. Stateless CSRF with JWT

Alternative Approach for JWT-based Apps:

// Generate CSRF token derived from JWT
const generateCsrfToken = (jwtToken) => {
  const secret = jwtToken.split('.')[2]; // Use signature part
  return crypto.createHmac('sha256', secret)
    .update('csrf-token')
    .digest('hex');
};

// Middleware to verify
const verifyCsrf = (req, res, next) => {
  const jwtToken = req.cookies.jwt;
  const csrfToken = req.headers['x-csrf-token'];

  if (!jwtToken || !csrfToken || csrfToken !== generateCsrfToken(jwtToken)) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  next();
};

Testing Your CSRF Protections

1. Manual Testing with cURL

# Should fail without CSRF token
curl -X POST https://yourapp.com/api/sensitive-action \
  -H "Content-Type: application/json" \
  -d '{"data":"value"}'

# Should succeed with CSRF token
curl -X POST https://yourapp.com/api/sensitive-action \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: valid-token" \
  -b "sessionId=abc123; X-CSRF-TOKEN=valid-token" \
  -d '{"data":"value"}'

2. Automated Security Tests

// Jest test example
describe('CSRF Protection', () => {
  it('should reject POST requests without CSRF token', async () => {
    const response = await request(app)
      .post('/api/sensitive-action')
      .send({ data: 'test' });
    expect(response.status).toBe(403);
  });

  it('should accept POST requests with valid CSRF token', async () => {
    const csrfResponse = await request(app).get('/api/csrf-token');
    const { token } = csrfResponse.body;

    const response = await request(app)
      .post('/api/sensitive-action')
      .set('X-CSRF-Token', token)
      .set('Cookie', csrfResponse.headers['set-cookie'])
      .send({ data: 'test' });

    expect(response.status).toBe(200);
  });
});

Best Practices Checklist

  1. [ ] Implement anti-CSRF tokens for state-changing requests
  2. [ ] Set SameSite=Strict or Lax for session cookies
  3. [ ] Use secure, HttpOnly cookies for token storage
  4. [ ] Enforce CSRF protection on all mutating endpoints
  5. [ ] Implement proper error handling for CSRF failures
  6. [ ] Add CSP headers to prevent unauthorized form actions
  7. [ ] Regularly rotate CSRF tokens (per-session is ideal)
  8. [ ] Consider double-submit cookie pattern for SPAs
  9. [ ] Test CSRF protections manually and automatically
  10. [ ] Educate team about CSRF risks and protections

Ongoing Maintenance

  1. Monitoring: Log CSRF validation failures for security auditing
  2. Updates: Keep dependencies (especially security middleware) updated
  3. Review: Periodically reassess CSRF protections as application evolves
  4. Training: Conduct security workshops for new team members

Leave a Reply

Your email address will not be published. Required fields are marked *