Conversation
Introduces CSRF protection using the Double-Submit Cookie pattern to the README, library docs, and cookbook. Adds a dedicated recipe for CSRF protection, updates feature tables, and provides configuration and usage examples for both backend and frontend integration.
Updated Cargo.toml and Cargo.lock to reference version 0.1.13 for all internal RustAPI crates. This ensures consistency across the workspace and prepares for the next release.
…amework-ergonomics-update Feature/framework ergonomics update 410a7ff
There was a problem hiding this comment.
Pull request overview
This pull request adds comprehensive documentation for CSRF protection functionality in the RustAPI framework. The changes introduce a new cookbook recipe explaining CSRF protection using the Double-Submit Cookie pattern, update feature documentation, and bump version numbers from 0.1.12 to 0.1.13.
Changes:
- Added complete CSRF protection recipe documentation with examples and best practices
- Updated feature tables and documentation in rustapi-extras crate and README
- Bumped all crate versions from 0.1.12 to 0.1.13 in workspace configuration
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/cookbook/src/recipes/csrf_protection.md | New comprehensive CSRF protection recipe with configuration, integration examples, and security considerations |
| docs/cookbook/src/crates/rustapi_extras.md | Added CSRF feature documentation with basic usage examples |
| docs/cookbook/src/SUMMARY.md | Added CSRF Protection recipe to table of contents |
| crates/rustapi-extras/src/lib.rs | Updated crate documentation to include CSRF feature and example |
| README.md | Added csrf feature to feature table |
| Cargo.toml | Version bump from 0.1.12 to 0.1.13 for all workspace crates |
| Cargo.lock | Updated lock file to reflect version changes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let config = CsrfConfig::new() | ||
| // Cookie settings | ||
| .cookie_name("csrf_token") // Default: "csrf_token" | ||
| .cookie_path("/") // Default: "/" |
There was a problem hiding this comment.
The method cookie_path() does not exist in the CsrfConfig implementation. There is no setter method for cookie_path in the config.rs file.
| .cookie_path("/") // Default: "/" |
| <form method="POST" action="/submit"> | ||
| <input type="hidden" name="csrf_token" value="{}" /> | ||
| <button type="submit">Submit</button> | ||
| </form> |
There was a problem hiding this comment.
This example is misleading. The CSRF middleware validates the token from the request header (X-CSRF-Token or the configured header name), not from form data. Including the token as a hidden input field in a traditional HTML form will not work with this middleware implementation. For HTML forms to work with this CSRF protection, JavaScript would be needed to read the cookie and include it as a header, or the form would need to be submitted via JavaScript/AJAX with the header included.
| <form method="POST" action="/submit"> | |
| <input type="hidden" name="csrf_token" value="{}" /> | |
| <button type="submit">Submit</button> | |
| </form> | |
| <button id="submit-btn" type="button">Submit</button> | |
| <script> | |
| document.getElementById('submit-btn').addEventListener('click', async () => {{ | |
| const response = await fetch('/submit', {{ | |
| method: 'POST', | |
| headers: {{ | |
| 'X-CSRF-Token': '{}' | |
| }} | |
| }}); | |
| const text = await response.text(); | |
| alert(text); | |
| }}); | |
| </script> |
| |--------------|--------| | ||
| | Token in cookie | ✅ HttpOnly=false (JS needs access) | | ||
| | Token validation | ✅ Constant-time comparison | | ||
| | SameSite cookie | ✅ Configurable (Strict by default) | |
There was a problem hiding this comment.
The default SameSite policy is incorrect. According to the implementation in config.rs line 55, the default is SameSite::Lax, not Strict.
| | SameSite cookie | ✅ Configurable (Strict by default) | | |
| | SameSite cookie | ✅ Configurable (Lax by default) | |
| | Consideration | Status | | ||
| |--------------|--------| | ||
| | Token in cookie | ✅ HttpOnly=false (JS needs access) | | ||
| | Token validation | ✅ Constant-time comparison | |
There was a problem hiding this comment.
The documentation claims constant-time comparison is used for token validation, but the actual implementation at layer.rs line 73 uses a regular string equality check (h_token == token.as_str()), which is vulnerable to timing attacks. This should use a constant-time comparison function to prevent timing attacks that could leak information about the token.
| | Token validation | ✅ Constant-time comparison | | |
| | Token validation | Uses regular equality check (not constant-time in this example) | |
| ```rust | ||
| let config = CsrfConfig::new() | ||
| // Cookie settings | ||
| .cookie_name("csrf_token") // Default: "csrf_token" |
There was a problem hiding this comment.
The default cookie name is incorrect. According to the actual implementation in crates/rustapi-extras/src/csrf/config.rs, the default cookie name is "XSRF-TOKEN", not "csrf_token".
| .header_name("X-CSRF-Token") // Default: "X-CSRF-Token" | ||
| .token_length(32); // Default: 32 bytes |
There was a problem hiding this comment.
The method token_length() does not exist in the CsrfConfig implementation. There is no setter method for token_length in the config.rs file.
| .header_name("X-CSRF-Token") // Default: "X-CSRF-Token" | |
| .token_length(32); // Default: 32 bytes | |
| .header_name("X-CSRF-Token"); // Default: "X-CSRF-Token" | |
| // Token length is fixed at 32 bytes by default and is not configurable. |
| let csrf_config = CsrfConfig::new() | ||
| .cookie_name("csrf_token") | ||
| .header_name("X-CSRF-Token") | ||
| .cookie_secure(true); // HTTPS only |
There was a problem hiding this comment.
The method name is incorrect. The actual method is secure(), not cookie_secure(). According to the implementation in config.rs, the builder method is named secure().
| .cookie_secure(true); // HTTPS only | |
| .secure(true); // HTTPS only |
| function getCsrfToken() { | ||
| return document.cookie | ||
| .split('; ') | ||
| .find(row => row.startsWith('csrf_token=')) |
There was a problem hiding this comment.
This example assumes the cookie name is "csrf_token", but the actual default cookie name is "XSRF-TOKEN" according to the implementation. The cookie name should match what is configured or use the default.
| .find(row => row.startsWith('csrf_token=')) | |
| .find(row => row.startsWith('XSRF-TOKEN=')) |
| For traditional form submissions, include the token as a hidden field: | ||
|
|
||
| ```html | ||
| <form method="POST" action="/api/submit"> | ||
| <input type="hidden" name="_csrf" value="{{ csrf_token }}" /> | ||
| <!-- form fields --> | ||
| <button type="submit">Submit</button> | ||
| </form> |
There was a problem hiding this comment.
This example is misleading. The CSRF middleware validates the token from the request header, not from form data. A hidden input field with name="_csrf" will not be validated by the middleware as implemented. For traditional HTML forms to work with this CSRF protection, JavaScript is needed to intercept the form submission and add the token as a header.
| For traditional form submissions, include the token as a hidden field: | |
| ```html | |
| <form method="POST" action="/api/submit"> | |
| <input type="hidden" name="_csrf" value="{{ csrf_token }}" /> | |
| <!-- form fields --> | |
| <button type="submit">Submit</button> | |
| </form> | |
| For traditional HTML form submissions, the CSRF middleware still expects the token in a request header (not in form data). Use JavaScript to intercept the submission, read the token, and send the request with the header: | |
| ```html | |
| <form id="csrf-form" method="POST" action="/api/submit"> | |
| <!-- form fields --> | |
| <button type="submit">Submit</button> | |
| </form> | |
| <script> | |
| document.getElementById('csrf-form').addEventListener('submit', function (event) { | |
| event.preventDefault(); | |
| const form = event.target; | |
| const formData = new FormData(form); | |
| // Read token from cookie (must match the cookie name set by the backend) | |
| const csrfToken = document.cookie | |
| .split('; ') | |
| .find(row => row.startsWith('csrf_token=')) | |
| ?.split('=')[1]; | |
| fetch(form.action, { | |
| method: form.method, | |
| body: formData, | |
| headers: { | |
| 'X-CSRF-Token': csrfToken, | |
| }, | |
| }); | |
| }); | |
| </script> |
| use cookie::SameSite; | ||
|
|
||
| let config = CsrfConfig::new() | ||
| .cookie_same_site(SameSite::Strict); // Most restrictive |
There was a problem hiding this comment.
The method name is incorrect. The actual method is same_site(), not cookie_same_site(). According to the implementation in config.rs, the builder method is named same_site().
| .cookie_same_site(SameSite::Strict); // Most restrictive | |
| .same_site(SameSite::Strict); // Most restrictive |
Description
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
Type of Change
Checklist
Related Issues
Fixes # (issue number)
Testing
Please describe the tests that you ran to verify your changes:
Screenshots (if applicable)
Add screenshots to help explain your changes.
Additional Notes
Add any other context about the pull request here.