Repository navigation
Block CSRF on [REDACTED SECRET] enable/disable + appointment confirm: POST-only, CSRF cookie token, SameSite=Lax (sfind-e783cad7) - #617
Conversation
…F (cookie token for AdminPortal), SameSite=Lax cookies (sfind-e783cad7)
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
|
||
| http | ||
| .csrf().disable().cors().disable() | ||
| .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) |
There was a problem hiding this comment.
🟥 Localhost pages can forge administrator actions
When an administrator visits another localhost page, XSRF-TOKEN remains readable across ports. That page can send credentialed POSTs with the token and change customer accounts or appointments.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Confirmed that this is possible, but I don't think it's a regression this PR introduces. Here's my analysis.
Why it happens
- Browser cookies are not separated by port. Any page on
localhost:*can readdocument.cookie, including the non-HttpOnlyXSRF-TOKEN. localhost:3000andlocalhost:8080count as the same site, soSameSite=Laxstill sends the session cookie on that page's POSTs.- A header-based request (
X-XSRF-TOKEN) from that page fails CORS preflight, becauseRequestFilteronly allowshttp://localhost:4200. A plain form POST with_csrf=<token>doesn't need preflight, so it would get through.
Why I'm treating it as residual risk
- The attacker must already be serving a page on the [REDACTED SECRET]'s own host. On
master, any website anywhere can do this with a single GET. - The finding's recommendation names
CookieCsrfTokenRepository.withHttpOnlyFalse()specifically. This is also Spring's standard SPA pattern, where the token is readable by design. - The
localhost:4200→localhost:8080URLs are the project's dev layout. A real deployment would put the portal and API on their own origins.
Option if you want this closed too
Store the token server-side (HttpSessionCsrfTokenRepository). Return it from GET /api/csrf in a response header that only the allowed CORS origin can read. Have AdminPortal attach X-XSRF-TOKEN to each POST itself, instead of relying on Angular's cookie strategy.
That changes the approach the finding asked for, so I've left it for the requester to decide rather than changing it in this PR.
Summary
Fixes code-scan finding
sfind-e783cad74fc547a4aa76517c19e01c5f: a forged GET (or a cross-site POST) could make a logged-in [REDACTED SECRET]'s browser disable or re-enable any customer, or confirm appointments. Two things allowed it: the handlers had no HTTP method set, andSecurityConfighad CSRF turned off.Backend (
UserFront)UserResource.enableUser/diableUserandAppointmentResource.confirmAppointmentnow accept onlyRequestMethod.POST, so a GET returns 405.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()). The AdminPortal SPA reads theXSRF-TOKENcookie, and Angular'sHttpsends it back asX-XSRF-TOKEN. That header is added toRequestFilter's CORS allow-headers.GET /api/csrf(CsrfResource, permitAll) returns 204. Its only job is to make the CSRF filter issue the cookie before the SPA's first POST (login).SameSiteCookieProcessor+SameSiteCookieConfig: every cookie Tomcat writes (JSESSIONID, remember-me, XSRF-TOKEN) now getsSameSite=Lax. Tomcat 8.5.15 has nosetSameSiteCookies, so the processor wrapsRfc6265CookieProcessorand appends the attribute./logoutis POST-only and the header link is now a form.name="${_csrf.parameterName}"hidden inputs were removed. They were literal, not Thymeleaf-evaluated.th:actionalready injects the real token.AdminPortal
user.service.ts/appointment.service.tsnow usehttp.post(url, null, {withCredentials: true}).login.service.tscallsGET /api/csrfbefore the login POST and before the logout POST.location.reload()ran right away and could cancel the request.Tests:
CsrfProtectionTest(@WebMvcTest+ spring-security-test) andSameSiteCookieProcessorTest.Verification
Maven Central is blocked here and the Boot 1.5.4 parent isn't cached, so I couldn't build this branch as-is. Instead I verified on a Boot 2.7.18 stand-in copy of this branch: the PR #474 pom/properties, plus a
WebServerFactoryCustomizerin place of the Boot 1.5EmbeddedServletContainerCustomizer. It ran against MySQL 5.7.CsrfProtectionTest+SameSiteCookieProcessorTest->OK (12 tests).masterfor comparison):GET /api/user/bob/disablePOST /api/user/bob/disable(no token)GET /api/appointment/1/confirmPOST /disable+X-XSRF-TOKENfrom/api/csrf/enable200Set-CookieJSESSIONID=…; HttpOnlyJSESSIONID=…; HttpOnly; SameSite=Lax(+XSRF-TOKEN)ng build(JIT) passes. Theng linterrors are all on lines this PR doesn't touch; they already exist onmaster.Overlap
This repeats earlier unmerged CSRF branches and PRs. It is close to #479 (
devin/sfind-7127523f-csrf-enable) and #127 (SameSite). Per the one-ticket-one-PR rule it is a new PR for this finding. Only one of them should land.Devin-Org: engineering
Link to Devin session: https://devin-gtm.devinenterprise.com/sessions/9fe283decb5c465e9950e9142c06fdfa
Open in Devin Desktop: https://devin-gtm.devinenterprise.com/desktop/session/9fe283decb5c465e9950e9142c06fdfa?variant=devin
Devin Review