Skip to content

Block CSRF on [REDACTED SECRET] enable/disable + appointment confirm: POST-only, CSRF cookie token, SameSite=Lax (sfind-e783cad7) - #617

Open
devin-ai-integration[bot] wants to merge 1 commit into
masterfrom
devin/sfind-e783cad7-admin-enable-disable-post-csrf
Open

devin-ai-integration[bot] wants to merge 1 commit into
masterfrom
devin/sfind-e783cad7-admin-enable-disable-post-csrf

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

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, and SecurityConfig had CSRF turned off.

Backend (UserFront)

  • UserResource.enableUser / diableUser and AppointmentResource.confirmAppointment now accept only RequestMethod.POST, so a GET returns 405.
  • CSRF is on again: csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()). The AdminPortal SPA reads the XSRF-TOKEN cookie, and Angular's Http sends it back as X-XSRF-TOKEN. That header is added to RequestFilter's CORS allow-headers.
  • New 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).
  • New SameSiteCookieProcessor + SameSiteCookieConfig: every cookie Tomcat writes (JSESSIONID, remember-me, XSRF-TOKEN) now gets SameSite=Lax. Tomcat 8.5.15 has no setSameSiteCookies, so the processor wraps Rfc6265CookieProcessor and appends the attribute.
  • Turning CSRF on also affects the Thymeleaf UI:
    • /logout is POST-only and the header link is now a form.
    • Recipient delete is a POST form; it used to be a GET link.
    • The name="${_csrf.parameterName}" hidden inputs were removed. They were literal, not Thymeleaf-evaluated. th:action already injects the real token.

AdminPortal

  • user.service.ts / appointment.service.ts now use http.post(url, null, {withCredentials: true}).
  • login.service.ts calls GET /api/csrf before the login POST and before the logout POST.
  • Components reload the page only after the request succeeds. Previously location.reload() ran right away and could cancel the request.

Tests: CsrfProtectionTest (@WebMvcTest + spring-security-test) and SameSiteCookieProcessorTest.

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 WebServerFactoryCustomizer in place of the Boot 1.5 EmbeddedServletContainerCustomizer. It ran against MySQL 5.7.

  • JUnit: CsrfProtectionTest + SameSiteCookieProcessorTest -> OK (12 tests).
  • Running app, [REDACTED SECRET] signed in, curl (same calls against master for comparison):
request master this PR
forged GET /api/user/bob/disable 200, bob disabled 405, unchanged
forged POST /api/user/bob/disable (no token) 200, bob disabled 403, unchanged
forged GET /api/appointment/1/confirm 200 405
POST /disable + X-XSRF-TOKEN from /api/csrf n/a 200, bob disabled, then /enable 200
login Set-Cookie JSESSIONID=…; HttpOnly JSESSIONID=…; HttpOnly; SameSite=Lax (+ XSRF-TOKEN)
  • AdminPortal ng build (JIT) passes. The ng lint errors are all on lines this PR doesn't touch; they already exist on master.

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

Status Commit
⚪ Not started —

Run Devin Review

Enable auto-review to review your pull requests automatically.

Devin Review (Staging)


Devin Review

…F (cookie token for AdminPortal), SameSite=Lax cookies (sfind-e783cad7)
@devin-ai-integration

Copy link
Copy Markdown
Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedmaven/​org.springframework.security/​spring-security-test@​4.2.3.RELEASE8910089100100

View full report

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review


http
.csrf().disable().cors().disable()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 read document.cookie, including the non-HttpOnly XSRF-TOKEN.
  • localhost:3000 and localhost:8080 count as the same site, so SameSite=Lax still sends the session cookie on that page's POSTs.
  • A header-based request (X-XSRF-TOKEN) from that page fails CORS preflight, because RequestFilter only allows http://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:8080 URLs 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.

Written by Devin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants