Describe the bug
Submitting the login form (POST /users/log_in) with a stale CSRF token raises Plug.CSRFProtection.InvalidCSRFTokenError and the user sees the default 403 error page.
The most common trigger for real users: log in, click the browser back button, the cached login form is re-submitted with the pre-login CSRF token. Lightning's UserAuth.new_session/2 calls configure_session(renew: true) |> clear_session(), so the old token no longer matches the (new, empty) session.
You can recreate this by opening the login page on two different tabs and then login on each page without refreshing. Currently, you get this Forbidden page
Sentry
LIGHTNING-NS — 3,397 events, 94 users, ongoing since 2024-11-11.
https://openfn.sentry.io/issues/6056897474/
Bot scanners account for a large share (top 2 IPs = ~54% of recent introspectable events, heavily concentrated Chrome 131 fingerprint, scans of bare GCP IPs), but real users hit it too.
Version number
v2.16.5
Expected behavior
When CSRF validation fails on a browser request, redirect to /users/log_in with a flash like "Your session expired. Please try again." instead of rendering the 403 page.
Proposed fix
- Add
LightningWeb.Plugs.CSRFProtectionWithFallback that wraps Plug.CSRFProtection.call/2 in try/rescue and on InvalidCSRFTokenError does put_flash |> redirect(to: ~p"/users/log_in") |> halt(). Use it in place of plug :protect_from_forgery in the :browser pipeline.
- Add
Plug.CSRFProtection.InvalidCSRFTokenError to Lightning.SentryEventFilter @ignored_exceptions (lib/lightning/sentry_event_filter.ex) so any remaining occurrences (non-browser pipelines) stop creating Sentry noise.
The rescue is not a security regression — the token still fails validation, the request is still rejected, the response just becomes a 302 to a GET instead of a 403 page.
Describe the bug
Submitting the login form (
POST /users/log_in) with a stale CSRF token raisesPlug.CSRFProtection.InvalidCSRFTokenErrorand the user sees the default 403 error page.The most common trigger for real users: log in, click the browser back button, the cached login form is re-submitted with the pre-login CSRF token. Lightning's
UserAuth.new_session/2callsconfigure_session(renew: true) |> clear_session(), so the old token no longer matches the (new, empty) session.You can recreate this by opening the login page on two different tabs and then login on each page without refreshing. Currently, you get this Forbidden page
Sentry
LIGHTNING-NS — 3,397 events, 94 users, ongoing since 2024-11-11.
https://openfn.sentry.io/issues/6056897474/
Bot scanners account for a large share (top 2 IPs = ~54% of recent introspectable events, heavily concentrated Chrome 131 fingerprint, scans of bare GCP IPs), but real users hit it too.
Version number
v2.16.5
Expected behavior
When CSRF validation fails on a browser request, redirect to
/users/log_inwith a flash like "Your session expired. Please try again." instead of rendering the 403 page.Proposed fix
LightningWeb.Plugs.CSRFProtectionWithFallbackthat wrapsPlug.CSRFProtection.call/2intry/rescueand onInvalidCSRFTokenErrordoesput_flash |> redirect(to: ~p"/users/log_in") |> halt(). Use it in place ofplug :protect_from_forgeryin the:browserpipeline.Plug.CSRFProtection.InvalidCSRFTokenErrortoLightning.SentryEventFilter@ignored_exceptions(lib/lightning/sentry_event_filter.ex) so any remaining occurrences (non-browser pipelines) stop creating Sentry noise.The rescue is not a security regression — the token still fails validation, the request is still rejected, the response just becomes a 302 to a GET instead of a 403 page.