Status: ✅ Enabled (Milestone 12.1 - Security Hardening) Date: 2026-02-16
Cross-Site Request Forgery (CSRF) protection is now ENABLED in Darkhold. This prevents attackers from performing unauthorized actions on behalf of authenticated users.
Before (VULNERABLE):
http.csrf(AbstractHttpConfigurer::disable); // TODO: we need to enable CSRFAfter (PROTECTED):
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");
http.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(requestHandler)
.ignoringRequestMatchers("/h2-console/**") // Dev only
);- Method: Cookie-based (
XSRF-TOKEN) - HttpOnly: False (allows JavaScript access)
- SameSite: Lax (default, prevents CSRF)
- Path:
/ - Validation: Automatic by Spring Security
WebSocketConfig.java updated with documentation:
- CSRF token must be sent in CONNECT frame
- Token validated by Spring Security automatically
- Uses same
XSRF-TOKENcookie
Spring Security automatically adds CSRF token to Thymeleaf forms:
<form th:action="@{/endpoint}" method="post">
<!-- CSRF token automatically added by Thymeleaf -->
<button type="submit">Submit</button>
</form>For non-Thymeleaf forms, use hidden input:
<form action="/endpoint" method="post">
<input type="hidden" name="_csrf" value="[token]" />
<button type="submit">Submit</button>
</form>// Include csrf-manager.js in page
<script src="/scripts/csrf-manager.js"></script>
// For XMLHttpRequest
let xhr = new XMLHttpRequest();
xhr.open('POST', '/endpoint', true);
CsrfManager.addTokenToXHR(xhr);
xhr.send(data);
// For Fetch API
fetch('/endpoint', {
method: 'POST',
headers: CsrfManager.addTokenToFetch({
'Content-Type': 'application/json'
}),
body: JSON.stringify(data)
});// Read token from cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
const token = getCookie('XSRF-TOKEN');
xhr.setRequestHeader('X-XSRF-TOKEN', token);// Include csrf-manager.js first
<script src="/scripts/csrf-manager.js"></script>
// In your connection code
let socket = new SockJS('/darkhold-websocket');
stompClient = Stomp.over(socket);
// Get CSRF headers
let headers = {};
if (typeof CsrfManager !== 'undefined') {
headers = CsrfManager.getHeadersForStomp();
}
// Connect with CSRF token
stompClient.connect(headers, function (frame) {
console.log('Connected: ' + frame);
// ... rest of connection logic
});- ✅
SecurityConfig.java- CSRF enabled with cookie repository - ✅
WebSocketConfig.java- Added CSRF documentation
- ✅
csrf-manager.js- NEW: CSRF token manager- Auto-protects forms on page load
- Provides helpers for AJAX/WebSocket
- Reads from
XSRF-TOKENcookie
- ✅
game-wait-scripts.js- Player lobby connection - ✅
game-scripts.js- 2 connections (showScoreboard, connect) - ✅
scoreboard-scripts.js- 2 connections (askQuestion, connect) - ✅
publish-scripts.js- 2 connections (startGame, connect) - ✅
question-scripts.js- 1 connection (connect)
- ✅
home-scripts.js- PIN validation (enterGame) - ⏳ Other AJAX calls need review
- Registration form submission
- Login form submission
- Quiz creation
- Quiz deletion
- Question CRUD operations
- User management operations
- Team creation/assignment
- PIN validation (
/enterGame) - Email validation
- File uploads
- Challenge import/export
- Player join lobby (
/app/user) - game-wait-scripts.js - Game start trigger (
/app/start) - publish-scripts.js - Question fetch (
/app/question_fetch) - question-scripts.js - Fetch scores (
/app/fetch_scores) - game-scripts.js - Next question (
/app/next_question) - scoreboard-scripts.js - Answer submission (via HTTP POST, not WebSocket)
- Pause/Resume game (via moderator controls)
- Skip question (via moderator controls)
- End game early (via moderator controls)
- Kick player (via moderator controls)
- Team assignment (via drag-drop UI)
<script type="text/javascript" src="/scripts/csrf-manager.js"></script>Before:
xhr.open('POST', '/endpoint', true);
xhr.send(data);After:
xhr.open('POST', '/endpoint', true);
CsrfManager.addTokenToXHR(xhr); // Add this line
xhr.send(data);Before:
stompClient.connect({}, function (frame) {
// connection logic
});After:
let headers = CsrfManager ? CsrfManager.getHeadersForStomp() : {};
stompClient.connect(headers, function (frame) {
// connection logic
});Symptoms: 403 Forbidden on POST/PUT/DELETE Solution: Ensure csrf-manager.js is loaded before making requests
Symptoms: WebSocket fails to connect, 403 in network tab Solution: Add CSRF headers to STOMP connect() call
Symptoms: 403 on form submit Solution:
- For Thymeleaf: Use
th:actioninstead of plainaction - For HTML: Add hidden
_csrfinput field
Symptoms: Intermittent 403 errors
Solution: Call CsrfManager.refresh() after long idle periods
H2 console is excluded from CSRF protection:
.ignoringRequestMatchers("/h2-console/**")Production: Remove H2 console entirely or secure with separate auth.
✅ DO:
- Use
th:actionfor all Thymeleaf forms - Include csrf-manager.js on all pages with AJAX
- Add CSRF headers to all WebSocket connections
- Test CSRF protection in development
❌ DON'T:
- Disable CSRF globally
- Skip CSRF on state-changing operations
- Expose CSRF tokens in URLs
- Use GET for state-changing operations
- Minimal: Token stored in cookie, validated once per request
- No database lookups: Token validation is cryptographic
- Caching: Works normally with CSRF enabled
- WebSocket: Single token validation on CONNECT
✅ OWASP Top 10: Addresses A01:2021 - Broken Access Control ✅ CWE-352: Cross-Site Request Forgery ✅ PCI DSS: Requirement 6.5.9 ✅ GDPR: Protects user actions and data integrity
- ✅
Complete WebSocket Updates: All 8 WebSocket connections updated - Test All Endpoints: Comprehensive testing of 35+ HTTP endpoints
- Update Documentation: Add CSRF examples to API docs
- Audit Templates: Ensure all forms use Thymeleaf
th:action - Security Testing: Attempt CSRF attacks to verify protection
Last Updated: 2026-02-16 Status: 🟢 Complete - Core protection enabled, all 8 WebSocket connections updated