-
Notifications
You must be signed in to change notification settings - Fork 297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
General
: Add csrf protection + sameSite none cookie
#9404
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -35,6 +35,7 @@ | |||||||||||||
|
||||||||||||||
import de.tum.cit.aet.artemis.core.security.DomainUserDetailsService; | ||||||||||||||
import de.tum.cit.aet.artemis.core.security.Role; | ||||||||||||||
import de.tum.cit.aet.artemis.core.security.filter.CsrfArtemisFilter; | ||||||||||||||
import de.tum.cit.aet.artemis.core.security.filter.SpaWebFilter; | ||||||||||||||
import de.tum.cit.aet.artemis.core.security.jwt.JWTConfigurer; | ||||||||||||||
import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; | ||||||||||||||
|
@@ -54,6 +55,8 @@ public class SecurityConfiguration { | |||||||||||||
|
||||||||||||||
private final CorsFilter corsFilter; | ||||||||||||||
|
||||||||||||||
private final CsrfArtemisFilter csrfArtemisFilter = new CsrfArtemisFilter(); | ||||||||||||||
|
||||||||||||||
private final ProfileService profileService; | ||||||||||||||
|
||||||||||||||
private final Optional<CustomLti13Configurer> customLti13Configurer; | ||||||||||||||
|
@@ -171,7 +174,9 @@ public SecurityFilterChain filterChain(HttpSecurity http, SecurityProblemSupport | |||||||||||||
// Disables CSRF (Cross-Site Request Forgery) protection; useful in stateless APIs where the token management is unnecessary. | ||||||||||||||
.csrf(AbstractHttpConfigurer::disable) | ||||||||||||||
// Adds a CORS (Cross-Origin Resource Sharing) filter before the username/password authentication to handle cross-origin requests. | ||||||||||||||
// sets the filter chain: CorsFilter -> CsrfArtemisFilter -> UsernamePasswordAuthenticationFilter | ||||||||||||||
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) | ||||||||||||||
.addFilterBefore(csrfArtemisFilter, UsernamePasswordAuthenticationFilter.class) | ||||||||||||||
Comment on lines
+177
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) LGTM: CSRF filter added correctly. Minor suggestion for comment improvement. The addition of the A minor suggestion to improve the comment: Consider updating the comment to be more explicit about the CSRF filter: -// sets the filter chain: CorsFilter -> CsrfArtemisFilter -> UsernamePasswordAuthenticationFilter
+// Sets the filter chain: CorsFilter -> CsrfArtemisFilter (for CSRF protection) -> UsernamePasswordAuthenticationFilter This change would provide more context about the purpose of the 📝 Committable suggestion
Suggested change
|
||||||||||||||
// Configures exception handling with a custom entry point and access denied handler for authentication issues. | ||||||||||||||
.exceptionHandling(handler -> handler.authenticationEntryPoint(securityProblemSupport).accessDeniedHandler(securityProblemSupport)) | ||||||||||||||
// Adds a custom filter for Single Page Applications (SPA), i.e. the client, after the basic authentication filter. | ||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,36 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
package de.tum.cit.aet.artemis.core.security.filter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import java.io.IOException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.servlet.FilterChain; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.servlet.ServletException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.servlet.http.HttpServletRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import jakarta.servlet.http.HttpServletResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import org.slf4j.Logger; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import org.slf4j.LoggerFactory; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.filter.OncePerRequestFilter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
public class CsrfArtemisFilter extends OncePerRequestFilter { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private static final Logger log = LoggerFactory.getLogger(CsrfArtemisFilter.class); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Check if the custom CSRF header is present in the request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (request.getHeader("X-ARTEMIS-CSRF") != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
filterChain.doFilter(request, response); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Reject the request if the header is missing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Missing CSRF protection header"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+18
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) LGTM with suggestions: Method implementation is correct but can be improved. The
Here's a suggested refactoring: public class CsrfArtemisFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(CsrfArtemisFilter.class);
+ private static final String CSRF_HEADER = "X-ARTEMIS-CSRF";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Check if the custom CSRF header is present in the request
- if (request.getHeader("X-ARTEMIS-CSRF") != null) {
+ if (request.getHeader(CSRF_HEADER) != null) {
filterChain.doFilter(request, response);
}
else {
// Reject the request if the header is missing
- response.sendError(HttpServletResponse.SC_FORBIDDEN, "Missing CSRF protection header");
+ log.warn("Rejected request due to missing CSRF header: {}", request.getRequestURI());
+ response.sendError(HttpStatus.FORBIDDEN.value(), "Missing CSRF protection header");
}
}
} These changes will improve code maintainability and provide better logging for security-related events. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// exclude all non api calls such as git, ... | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Override | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return !request.getRequestURI().startsWith("/api/"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+31
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) LGTM with a minor suggestion: Method implementation is correct. The // exclude all non api calls such as git, ...
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
- return !request.getRequestURI().startsWith("/api/");
+ return !request.getRequestURI().startsWith("/api/"); // Only filter API calls
} This inline comment makes the method's purpose even more explicit at a glance. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,11 +5,8 @@ | |
|
||
import java.time.Duration; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.Arrays; | ||
import java.util.Collection; | ||
|
||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.core.env.Environment; | ||
import org.springframework.http.ResponseCookie; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.stereotype.Service; | ||
|
@@ -18,15 +15,10 @@ | |
@Service | ||
public class JWTCookieService { | ||
|
||
private static final String DEVELOPMENT_PROFILE = "dev"; | ||
|
||
private final TokenProvider tokenProvider; | ||
|
||
private final Environment environment; | ||
|
||
public JWTCookieService(TokenProvider tokenProvider, Environment environment) { | ||
public JWTCookieService(TokenProvider tokenProvider) { | ||
this.tokenProvider = tokenProvider; | ||
this.environment = environment; | ||
} | ||
|
||
/** | ||
|
@@ -59,12 +51,9 @@ public ResponseCookie buildLogoutCookie() { | |
*/ | ||
private ResponseCookie buildJWTCookie(String jwt, Duration duration) { | ||
|
||
Collection<String> activeProfiles = Arrays.asList(environment.getActiveProfiles()); | ||
boolean isSecure = !activeProfiles.contains(DEVELOPMENT_PROFILE); | ||
|
||
return ResponseCookie.from(JWT_COOKIE_NAME, jwt).httpOnly(true) // Must be httpOnly | ||
.sameSite("Lax") // Must be Lax to allow navigation links to Artemis to work | ||
.secure(isSecure) // Must be secure | ||
.sameSite("None") // Must be None to allow cross-site requests to Artemis from the VS Code plugin | ||
.secure(true) // Must be secure to allow sameSite=None | ||
Comment on lines
+55
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Approve cookie security changes with a suggestion for additional documentation. The changes to set Consider adding a comment about the potential security implications of using Example: // Setting sameSite to "None" introduces potential CSRF vulnerabilities,
// which are mitigated by the custom X-ARTEMIS-CSRF header implementation. |
||
.path("/") // Must be "/" to be sent in ALL request | ||
.maxAge(duration) // Duration should match the duration of the jwt | ||
.build(); // Build cookie | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,8 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo | |
import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; | ||
import { artemisIconPack } from 'src/main/webapp/content/icons/icons'; | ||
import { ScrollingModule } from '@angular/cdk/scrolling'; | ||
import { HTTP_INTERCEPTORS } from '@angular/common/http'; | ||
import { CsrfInterceptor } from 'app/core/csrf/csrf.interceptor'; | ||
|
||
// NOTE: this module should only include the most important modules for normal users, all course management, admin and account functionality should be lazy loaded if possible | ||
@NgModule({ | ||
|
@@ -59,6 +61,7 @@ import { ScrollingModule } from '@angular/cdk/scrolling'; | |
SystemNotificationComponent, | ||
LoadingNotificationComponent, | ||
], | ||
providers: [{ provide: HTTP_INTERCEPTORS, useClass: CsrfInterceptor, multi: true }], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) LGTM: CSRF interceptor correctly added as a provider. The Consider moving the provider to a separate line for better readability: providers: [
{ provide: HTTP_INTERCEPTORS, useClass: CsrfInterceptor, multi: true }
], |
||
bootstrap: [JhiMainComponent], | ||
}) | ||
export class ArtemisAppModule { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; | ||
import { Observable } from 'rxjs'; | ||
|
||
export class CsrfInterceptor implements HttpInterceptor { | ||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | ||
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi | ||
// Clone the request and add the CSRF token header | ||
const csrfReq = req.clone({ headers: req.headers.set('X-ARTEMIS-CSRF', 'Dennis ist schuld') }); | ||
|
||
return next.handle(csrfReq); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider using constructor injection for CsrfArtemisFilter.
While the field declaration is correct and follows naming conventions, consider using constructor injection for
CsrfArtemisFilter
instead of instantiating it inline. This approach would improve testability and align with the dependency injection principle used elsewhere in this class.You could modify the code as follows: