Preventing Clickjacking in React Apps

Loading

Clickjacking (UI redress attack) is a malicious technique where attackers trick users into clicking something different from what they perceive. Here’s how to secure your React applications against clickjacking attacks:

Core Defense Mechanisms

1. Frame-Busting Headers

// In your server configuration (Express.js example)
app.use(helmet.frameguard({ action: 'deny' }));

// Equivalent to setting:
// X-Frame-Options: DENY

2. Content Security Policy (CSP)

<!-- index.html -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none';">

3. Client-Side Protection

// Frame-busting script (additional layer of defense)
useEffect(() => {
  if (window !== window.top) {
    window.top.location = window.location;
  }
}, []);

Implementation Strategies

1. Server-Side Headers (Best Practice)

# Nginx configuration
add_header X-Frame-Options "DENY";
add_header Content-Security-Policy "frame-ancestors 'none'";

2. React-Specific Solutions

Protected Routes Component

const FrameGuard = ({ children }) => {
  useEffect(() => {
    if (window.self !== window.top) {
      document.body.style.display = 'none';
      window.top.location = window.self.location;
    }
    return () => {
      document.body.style.display = 'block';
    };
  }, []);

  return <>{children}</>;
};

// Usage
<FrameGuard>
  <ProtectedRoute />
</FrameGuard>

Conditional Rendering

function App() {
  const [isFramed, setIsFramed] = useState(false);

  useEffect(() => {
    setIsFramed(window.self !== window.top);
  }, []);

  return isFramed ? (
    <div className="clickjacking-warning">
      <h1>Security Alert</h1>
      <p>This page cannot be displayed in a frame.</p>
      <button onClick={() => (window.location.href = window.top.location.href)}>
        Continue to site
      </button>
    </div>
  ) : (
    <MainApp />
  );
}

Advanced Protection Techniques

1. Dynamic CSP for Development

// webpack.config.js (for create-react-app eject)
const isProduction = process.env.NODE_ENV === 'production';

new HtmlWebpackPlugin({
  meta: {
    'Content-Security-Policy': {
      'http-equiv': 'Content-Security-Policy',
      'content': isProduction 
        ? "frame-ancestors 'none';" 
        : "frame-ancestors 'self';"
    }
  }
});

2. Session-Based Frame Protection

// Server middleware
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', req.session?.allowedFraming ? 'ALLOW-FROM trusted.com' : 'DENY');
  next();
});

3. Visual Protection for Sensitive Actions

const SecureButton = ({ onClick, children }) => {
  const [requiresConfirmation, setRequiresConfirmation] = useState(false);

  const handleClick = (e) => {
    if (window.self !== window.top) {
      e.preventDefault();
      setRequiresConfirmation(true);
      return;
    }
    onClick(e);
  };

  return (
    <>
      <button onClick={handleClick}>{children}</button>
      {requiresConfirmation && (
        <div className="secure-confirm-overlay">
          <div className="secure-confirm-box">
            <p>Security confirmation required</p>
            <button onClick={() => setRequiresConfirmation(false)}>
              Cancel
            </button>
            <button onClick={() => {
              window.top.location = window.self.location;
            }}>
              Continue
            </button>
          </div>
        </div>
      )}
    </>
  );
};

Testing Your Defenses

1. Manual Testing

<!-- Create a test.html file -->
<iframe src="http://your-react-app.com"></iframe>

2. Automated Security Headers Check

// Jest test example
test('should have clickjacking protection headers', async () => {
  const response = await fetch('http://localhost:3000');
  expect(response.headers.get('X-Frame-Options')).toBe('DENY');
  expect(response.headers.get('Content-Security-Policy')).toContain("frame-ancestors 'none'");
});

3. Browser Extension Tests

  • Use “XSS Me” or “Security Headers” browser extensions
  • Check your site’s headers at securityheaders.com

Special Cases and Workarounds

1. Allowing Specific Iframe Embeds

// For trusted domains only
app.use(helmet.frameguard({
  action: 'allow-from',
  domain: 'https://trusted-platform.com'
}));

// CSP equivalent
<meta http-equiv="Content-Security-Policy" content="frame-ancestors https://trusted-platform.com">

2. Legacy Browser Support

// Combine modern and legacy techniques
const ClickjackProtection = () => {
  useEffect(() => {
    // Modern browsers
    try {
      if (window.self !== window.top) {
        window.top.location = window.self.location;
      }
    } catch (e) {
      // Legacy browsers fallback
      document.body.innerHTML = '';
      document.write('This page cannot be framed.');
    }

    // Additional style protection
    document.body.style.display = 'none';
    if (window.self === window.top) {
      document.body.style.display = 'block';
    }
  }, []);

  return null;
};

Best Practices Checklist

  1. [ ] Implement X-Frame-Options: DENY header
  2. [ ] Set CSP frame-ancestors 'none' directive
  3. [ ] Add client-side frame-busting as secondary defense
  4. [ ] Protect sensitive actions with visual confirmation
  5. [ ] Test with iframe embedding attempts
  6. [ ] Monitor security headers regularly
  7. [ ] Educate team about clickjacking risks
  8. [ ] Document framing policies for your application

Ongoing Maintenance

  1. Header Monitoring
   curl -I https://yourapp.com | grep -iE 'x-frame-options|csp'
  1. Automated Security Scanning
   # GitHub Actions example
   - name: Security scan
     uses: OWASP/glue@v1
     with:
       target: http://localhost:3000
       scanner: arachni
  1. Policy Review
  • Quarterly review of framing requirements
  • Update allowed domains list as needed
  • Remove legacy exceptions when possible

Leave a Reply

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