XSRF and Restricted Browsers

A lot of my users have very restricted browsers. Most are on chrome. When the security is more than medium, they’re getting XSRF errors. Sometimes I can reduce this down and it will let things work but some of them have extra controls by parents (the hint I’ve found here is they’re not able to enter private browsing) and that means even turning off all security in chrome they still get the xsrf issues. Normally installing FireFox avoids this - but I feel (a) this isn’t the best solution and (b) it’s just waiting for the next iteration of FF to start listening to parental constraints and I’m up the proverbial river.

What I really want to do is just turn off XSRF in some way shape or form.

Any suggestions? I’m using the littlest jupyter hub if that makes a difference.

thanks

Could you tell us what issues are you facing exactly? Preferably with some logs. JupyterHub uses _xsrf cookies to make POST requests and if there is a proxy sitting infront is forwarding the remote IPs correctly, _xsrf cookies should work as intended. More info in docs.

1 Like

What logs would you like?

I’m happy to try those options in docs - my main issue is where do you do those things?

proxy_set_header << where does this go?
export JUPYTERHUB_XSRF_ANONYMOUS_IP_CIDRS=“10.0.0.0/8;172.16.0.0/12;192.168.0.0/16” << where is this set?

The other problem I have is replicating the issues of course - my students are all over the world and sometimes it takes weeks to arrange a call with them :-/ I’ll try and increase my own chrome security I guess to see if that does it…

Your hub logs where the you see the _xsrf errors. You can look at tljh docs on how to access logs. Also providing more background on how your JupyterHub deployment helps to find potential issues.

If you have deployed a reverse proxy (like nginx or Apache), proxy_set_header will go into the server block of the config of reverse proxy. JUPYTERHUB_XSRF_ANONYMOUS_IP_CIDRS environment variable must be set on JupyterHub systemd service. You can consult docs on how to customize the systemd services to add new environment variables

OK, I’ve remoted into a windows box, pushed security up and then tried logging in.

pulled up the end of the log - Im assuming this is the right one. I normally see the errors on the kids’ screens! My username is “oli” - looks like there may be others online at the moment as well.

May 22 08:48:21 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:21.180 JupyterHub log:192] 200 GET /hub/api/ (cull-idle@127.0.0.1) 22.67ms
May 22 08:48:21 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:21.189 JupyterHub log:192] 200 GET /hub/api/users?state=[secret] (cull-idle@127.0.0.1) 6.72ms
May 22 08:48:33 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:33.126 JupyterHub log:192] 302 GET /user/bdosremediosfurtado/lab/tree/Computing%20Assignment/Wolsey>
May 22 08:48:34 jupyterhub-2 python3[819109]: [W 2025-05-22 08:48:34.067 JupyterHub base:478] Invalid or expired cookie token
May 22 08:48:34 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:34.068 JupyterHub log:192] 302 GET /hub/user/bdosremediosfurtado/lab/tree/Computing%20Assignment/Wo>
May 22 08:48:35 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:35.366 JupyterHub _xsrf_utils:125] Setting new xsrf cookie for b'None:X4g5nuomdcN6l7OudmB6SZLXL8qGP>
May 22 08:48:35 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:35.369 JupyterHub log:192] 200 GET /hub/login?next=%2Fhub%2Fuser%2Fbdosremediosfurtado%2Flab%2Ftree>
May 22 08:48:43 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:43.695 JupyterHub _xsrf_utils:125] Setting new xsrf cookie for b'X4g5nuomdcN6l7OudmB6SZLXL8qGPU8kLP>
May 22 08:48:43 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:43.696 JupyterHub base:937] User logged in: bdosremediosfurtado
May 22 08:48:43 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:43.697 JupyterHub log:192] 302 POST /hub/login?next=%2Fhub%2Fuser%2Fbdosremediosfurtado%2Flab%2Ftre>
May 22 08:48:45 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:45.122 JupyterHub _xsrf_utils:125] Setting new xsrf cookie for b'd091295cfbdf41c4b552d98af53939be:9>
May 22 08:48:45 jupyterhub-2 python3[819109]: [W 2025-05-22 08:48:45.124 JupyterHub log:192] 424 GET /hub/user/bdosremediosfurtado/lab/tree/Computing%20Assignment/Wo>
May 22 08:48:50 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:50.966 JupyterHub provider:660] Creating oauth client jupyterhub-user-bdosremediosfurtado
May 22 08:48:51 jupyterhub-2 python3[1892221]: Running as unit: jupyter-bdosremediosfurtado.service; invocation ID: 7b112778181c48dbb1140a77206b1d54
May 22 08:48:51 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:51.936 JupyterHub log:192] 302 GET /hub/spawn/bdosremediosfurtado?next=%2Fhub%2Fuser%2Fbdosremedios>
May 22 08:48:52 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:52.354 JupyterHub pages:399] bdosremediosfurtado is pending spawn
May 22 08:48:52 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:52.356 JupyterHub log:192] 200 GET /hub/spawn-pending/bdosremediosfurtado?next=%2Fhub%2Fuser%2Fbdos>
May 22 08:48:53 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:53.370 JupyterHub log:192] 200 GET /hub/api (@127.0.0.1) 1.99ms
May 22 08:48:53 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:53.519 JupyterHub log:192] 200 POST /hub/api/users/bdosremediosfurtado/activity (bdosremediosfurtad>
May 22 08:48:54 jupyterhub-2 python3[819109]: [W 2025-05-22 08:48:54.339 JupyterHub _version:38] Single-user server has no version header, which means it is likely <>
May 22 08:48:54 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:54.339 JupyterHub base:1090] User bdosremediosfurtado took 3.402 seconds to start
May 22 08:48:54 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:54.340 JupyterHub proxy:331] Adding user bdosremediosfurtado to proxy /user/bdosremediosfurtado/ =>>
May 22 08:48:54 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:54.619 JupyterHub users:776] Server bdosremediosfurtado is ready
May 22 08:48:54 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:54.620 JupyterHub log:192] 200 GET /hub/api/users/bdosremediosfurtado/server/progress?_xsrf=[secret>
May 22 08:48:54 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:54.978 JupyterHub log:192] 302 GET /hub/spawn-pending/bdosremediosfurtado?next=%2Fhub%2Fuser%2Fbdos>
May 22 08:48:56 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:56.057 JupyterHub log:192] 302 GET /hub/user/bdosremediosfurtado/lab/tree/Computing%20Assignment/Wo>
May 22 08:48:56 jupyterhub-2 python3[819109]: [W 2025-05-22 08:48:56.439 JupyterHub log:192] 403 GET /hub/api/user (@127.0.0.1) 2.49ms
May 22 08:48:56 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:56.918 JupyterHub log:192] 302 GET /hub/api/oauth2/authorize?client_id=jupyterhub-user-bdosremedios>
May 22 08:48:57 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:57.420 JupyterHub log:192] 200 POST /hub/api/oauth2/token (bdosremediosfurtado@127.0.0.1) 37.55ms
May 22 08:48:57 jupyterhub-2 python3[819109]: [I 2025-05-22 08:48:57.438 JupyterHub log:192] 200 GET /hub/api/user (bdosremediosfurtado@127.0.0.1) 14.65ms
May 22 08:49:26 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:26.221 JupyterHub log:192] 302 GET / -> /hub/ (@172.69.224.219) 0.69ms
May 22 08:49:26 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:26.242 JupyterHub log:192] 302 GET /hub/ -> /hub/login?next=%2Fhub%2F (@172.69.224.219) 0.78ms
May 22 08:49:26 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:26.286 JupyterHub _xsrf_utils:125] Setting new xsrf cookie for b'None:f4AFjD-nk_OUm9MkZsnf4jPmeoPWG>
May 22 08:49:26 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:26.288 JupyterHub log:192] 200 GET /hub/login?next=%2Fhub%2F (@141.101.98.147) 2.68ms
May 22 08:49:33 jupyterhub-2 python3[819109]: [W 2025-05-22 08:49:33.033 JupyterHub base:943] Failed login for oli
May 22 08:49:33 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:33.035 JupyterHub log:192] 200 POST /hub/login?next=%2Fhub%2F (@141.101.98.147) 316.77ms
May 22 08:49:41 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:41.253 JupyterHub log:192] 302 GET / -> /hub/ (@172.68.229.122) 0.82ms
May 22 08:49:41 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:41.278 JupyterHub log:192] 302 GET /hub/ -> /hub/login?next=%2Fhub%2F (@172.68.229.122) 0.77ms
May 22 08:49:41 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:41.299 JupyterHub _xsrf_utils:125] Setting new xsrf cookie for b'None:arAdgcMDSoUFQdOE-EZDsLarbqcBP>
May 22 08:49:41 jupyterhub-2 python3[819109]: [I 2025-05-22 08:49:41.301 JupyterHub log:192] 200 GET /hub/login?next=%2Fhub%2F (@172.68.229.122) 2.08ms
May 22 08:50:03 jupyterhub-2 python3[819109]: [I 2025-05-22 08:50:03.891 JupyterHub _xsrf_utils:125] Setting new xsrf cookie for b'None:j2-cCR5eD9eVQMNW3g-pqtFfi8Zdd>
May 22 08:50:03 jupyterhub-2 python3[819109]: [W 2025-05-22 08:50:03.892 JupyterHub web:1873] 403 POST /hub/login?next=%2Fhub%2F (172.69.224.219): XSRF cookie does n>
May 22 08:50:03 jupyterhub-2 python3[819109]: [W 2025-05-22 08:50:03.893 JupyterHub log:192] 403 POST /hub/login?next=%2Fhub%2F (@172.69.224.219) 4.76ms
May 22 08:50:16 jupyterhub-2 python3[819109]: [I 2025-05-22 08:50:16.745 JupyterHub _xsrf_utils:125] Setting new xsrf cookie for b'None:NmowNKtTFKyWmZKSK0LXZuQgfTtnk>
May 22 08:50:16 jupyterhub-2 python3[819109]: [I 2025-05-22 08:50:16.749 JupyterHub log:192] 200 GET /hub/login?next=%2Fhub%2F (@172.70.162.251) 5.03ms
May 22 08:50:44 jupyterhub-2 python3[819109]: [I 2025-05-22 08:50:44.264 JupyterHub log:192] 200 POST /hub/api/users/jchoo/activity (jchoo@127.0.0.1) 17.05ms

If this is useful I’m happy to start looking at trying to do the proxy_set_header next - but I think that’s set up automatically by TLJH using Traefik?

Assuming this is JupyterHub 5.3.0, can you set

JUPYTERHUB_XSRF_ANONYMOUS_IP_CIDRS=0.0.0.0/0
JUPYTERHUB_XSRF_ANONYMOUS_ID_HEADERS=

in the hub env? This should effectively disable JupyterHub’s attempts to tie the xsrf cookie to the requesting browser, which I’m guessing the browser’s tracking prevention is preventing.

Disabling this introduces a cookie-tossing vulnerability, so it’s not ideal, but also not the worst thing in the world if your deployment doesn’t share a root domain with untrusted applications.

1 Like

Any other suggestions you can think of? We’ve got this set and are still encountering this issue :frowning:

Using AWS ALB Ingress on EKS, and GenericOAuthenticator with Okta

Thank you for all of your help on this project as well (and to me personally in the past!)

jupyterhub==5.3.0

z2jh helm chart 4.2.0

Can you share your full configuration, and your hub logs with debug logging enabled?

Hi @manics I’m @seanturner026’s colleague. Here’s the information you requested. I tried to preserve as much as I could of the relevant pieces (hub, ingress, and proxy config) without leaving in any sensitive information. Let me know if there is any additional context I can provide for you if this is insufficient.

Configuration in values.yaml:

hub:
  config:
    GenericOAuthenticator:
      ...
    JupyterHub:
      authenticator_class: generic-oauth
  existingSecret: hub-secrets
  extraEnv:
    JUPY_COOKIE_SECRET:
      valueFrom:
        secretKeyRef: ...
    JUPYTERHUB_CRYPT_KEY:
      valueFrom:
        secretKeyRef: ...
    JUPYTERHUB_XSRF_ANONYMOUS_IP_CIDRS: "0.0.0.0/0"
    JUPYTERHUB_XSRF_ANONYMOUS_ID_HEADERS: ""
    OAUTH_CLIENT_ID:
      valueFrom:
        secretKeyRef: ...
    OAUTH_CLIENT_SECRET:
      valueFrom:
        secretKeyRef: ...
    PGPASSWORD:
      valueFrom:
        secretKeyRef: ...
  image:
    name: ...
    tag: ...
  redirectToServer: False
  service:
    extraPorts:
      - port: 9888
        targetPort: 9888
        name: announcement
  services:
    gallery:
      name: gallery
      oauth_no_confirm: True
      url: http://hub-gallery.jupyterhub.svc.cluster.local:8080
    annoncement:
      name: announcement
      oauth_no_confirm: True
      url: http://hub.jupyterhub.svc.cluster.local:9888
      command: ["python", "-m", "jupyterhub_announcement"]
...
ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internal
    alb.ingress.kubernetes.io/target-type: instance
    alb.ingress.kubernetes.io/healthcheck-path: /hub/api
    alb.ingress.kubernetes.io/certificate-arn: ...
    alb.ingress.kubernetes.io/backend-protocol: HTTP
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    alb.ingress.kubernetes.io/group.name: ...
    external-dns.alpha.kubernetes.io/hostname: ...
  hosts:
    ...
...
proxy:
  secretToken: ...
  annotations: ...
  chp:
    networkPolicy:
      enabled: false
  service:
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp
      service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600"
      service.beta.kubernetes.io/aws-load-balancer-ssl-cert: ...
      service.beta.kubernetes.io/aws-load-balancer-ssl-ports: https
    type: NodePort
    nodePorts:
      http: 30080
      https: 30443

Please note a few things:

  • we are setting JUPYTERHUB_XSRF_ANONYMOUS_IP_CIDRS and JUPYTERHUB_XSRF_ANONYMOUS_ID_HEADERS per the suggestion by mnirk above. I’ve confirmed these environment variables are present in the running hub pod as well.
  • we are using alb in AWS as our ingress, which seems to behave a bit differently than other ingresses (eg X-Forwarded-For header is enabled by default and ours is configured for “Append”, and headers do not appear to be customizable like they are for nginx)

Hub logs related to XSRF cookies. This one captures me encountering the issue, logging out, then logging back in which fixed the issue:

[D 2025-07-30 21:30:49.586 JupyterHub base:411] Refreshing auth for julian
[I 2025-07-30 21:30:49.587 JupyterHub log:192] 302 GET /hub/ -> /hub/home (julian@::ffff:IP_ADDRESS_1) 194.21ms
[D 2025-07-30 21:30:49.726 JupyterHub _xsrf_utils:161] xsrf id mismatch b'n82<redacted>6Mc=:192<redacted>dd' != b'l1x<redacted>86Vk=:1921f<redacted>8add'
[I 2025-07-30 21:30:49.726 JupyterHub _xsrf_utils:130] Setting new xsrf cookie for b'l1x<redacted>86Vk=:1921f<redacted>8add' {'path': '/hub/'}
[D 2025-07-30 21:30:51.194 JupyterHub scopes:1013] Checking access to /hub/spawn/julian via scope servers!server=julian/
[D 2025-07-30 21:30:51.194 JupyterHub pages:208] Serving options form for julian
[D 2025-07-30 21:30:51.194 JupyterHub _xsrf_utils:161] xsrf id mismatch b'l1x<redacted>86Vk=:1921f<redacted>8add' != b'n82<redacted>v6Mc=:1921f<redacted>8add'
[I 2025-07-30 21:30:51.194 JupyterHub _xsrf_utils:130] Setting new xsrf cookie for b'n82<redacted>v6Mc=:1921f<redacted>8add' {'path': '/hub/'}
[D 2025-07-30 21:31:03.964 JupyterHub _xsrf_utils:161] xsrf id mismatch b'n82<redacted>v6Mc=:1921f<redacted>8add' != b'-b8<redacted>lXBI=:1921f<redacted>8add'
[I 2025-07-30 21:31:03.964 JupyterHub _xsrf_utils:130] Setting new xsrf cookie for b'-b8<redacted>lXBI=:1921f<redacted>8add' {'path': '/hub/'}
[W 2025-07-30 21:31:03.965 JupyterHub web:1873] 403 POST /hub/spawn/julian?_xsrf=MnwxOjB8MT<redacted>3YmMy (::ffff:IP_ADDRESS_1): XSRF cookie does not match POST argument
[D 2025-07-30 21:31:03.965 JupyterHub base:1542] Using default error template for 403
[W 2025-07-30 21:31:03.979 JupyterHub log:192] 403 POST /hub/spawn/julian?_xsrf=[secret] (julian@::ffff:IP_ADDRESS_1) 98.96ms
[D 2025-07-30 21:31:07.966 JupyterHub _xsrf_utils:161] xsrf id mismatch b'-b8<redacted>lXBI=:1921f<redacted>8add' != b'l1x<redacted>86Vk=:1921f<redacted>8add'
[I 2025-07-30 21:31:07.966 JupyterHub _xsrf_utils:130] Setting new xsrf cookie for b'l1x<redacted>86Vk=:1921f<redacted>8add' {'path': '/hub/'}
[I 2025-07-30 21:31:09.682 JupyterHub login:46] User logged out: julian
[I 2025-07-30 21:31:09.765 JupyterHub log:192] 302 GET /hub/logout -> /hub/login (@::ffff:IP_ADDRESS_2) 165.50ms
[I 2025-07-30 21:31:09.818 JupyterHub _xsrf_utils:130] Setting new xsrf cookie for b'None:-b8D8<redacted>XBI=' {'path': '/hub/', 'max_age': 3600}
[I 2025-07-30 21:31:09.818 JupyterHub _xsrf_utils:130] Setting new xsrf cookie for b'None:-b8D8<redacted>XBI=' {'path': '/hub/', 'max_age': 3600}

  ....

[D 2025-07-30 22:07:41.961 JupyterHub roles:326] Assigning default role to User julian
[D 2025-07-30 22:07:42.003 JupyterHub base:681] Setting cookie jupyterhub-session-id: {'httponly': True, 'path': '/'}
[D 2025-07-30 22:07:42.003 JupyterHub base:685] Setting cookie for julian: jupyterhub-hub-login
[D 2025-07-30 22:07:42.003 JupyterHub base:681] Setting cookie jupyterhub-hub-login: {'httponly': True, 'path': '/hub/'}
[I 2025-07-30 22:07:42.003 JupyterHub _xsrf_utils:130] Setting new xsrf cookie for b'l1x<redacted>86Vk=:1921f<redacted>8add' {'path': '/hub/'}
[I 2025-07-30 22:07:42.003 JupyterHub base:973] User logged in: julian
[I 2025-07-30 22:07:42.004 JupyterHub log:192] 302 GET /hub/oauth_callback?code=[secret]&state=[secret] -> /hub/home (julian@::ffff:IP_ADDRESS_3) 980.23ms
[D 2025-07-30 22:07:42.212 JupyterHub _xsrf_utils:161] xsrf id mismatch b'l1x<redacted>86Vk=:1921f<redacted>8add' != b'044<redacted>d6f54:1921f<redacted>8add'
[I 2025-07-30 22:07:42.212 JupyterHub _xsrf_utils:130] Setting new xsrf cookie for b'044<redacted>d6f54:1921f<redacted>8add' {'path': '/hub/'}
[D 2025-07-30 22:08:37.337 JupyterHub proxy:925] Proxy: Fetching GET http://proxy-api:8001/api/routes
[D 2025-07-30 22:08:37.339 JupyterHub proxy:392] Checking routes
[D 2025-07-30 22:08:56.727 JupyterHub scopes:1013] Checking access to /hub/spawn/julian via scope servers!server=julian/
[D 2025-07-30 22:08:56.727 JupyterHub pages:208] Serving options form for julian
[D 2025-07-30 22:09:11.757 JupyterHub scopes:1013] Checking access to /hub/spawn/julian via scope servers!server=julian/
[D 2025-07-30 22:09:11.758 JupyterHub pages:256] Triggering spawn with supplied form options for julian

  ....

[I 2025-07-30 22:09:12.343 JupyterHub log:192] 302 POST /hub/spawn/julian?_xsrf=[secret] -> /hub/spawn-pending/julian?_xsrf=[secret] (julian@::ffff:IP_ADDRESS_2) 680.67ms

I’ve redacted potentially sensitive information, namely:

  • full cookies (but left the begin/end so you can compare)
  • IP addresses (matching ones sharing the same number)

One thing I noticed debugging this is that for failed requests (with the XSRF error) the browser does not include jupyterhub-session-id cookie in the session. Instead, there is ajs_anonymous_id. Not sure if that is relevant but one thing that stood out to me.

Another thing to point out is that I’m surprised we are seeing the cookie comparison happening at all, since I thought minrk’s suggestion would disable this. But maybe it is working as expected and that’s just not surfaced in logs.

Anyway, grateful for any advice or ideas you have here! This has been stumping us for a while now and, while its an easy fix with a logout/login, it is affecting a lot of our users and is generally just very annoying. We started noticing it when we upgraded to Jupyterhub 5.3.0 (helm version 4.2.0) from helm version 3.2.1.

Thanks very much!

1 Like

Are you on an IPv6 network? Try using ::/0

1 Like

@manics thanks, good suggestion. We tried this yesterday with this:

    JUPYTERHUB_XSRF_ANONYMOUS_IP_CIDRS: "0.0.0.0/0;::/0"

We are still seeing this error pop up in logs, for example:

[W 2025-08-01 17:48:40.430 JupyterHub web:1873] 403 PUT /user/julian/lab/api/workspaces/default?1754070520409 (::ffff:<IPv4_address>): XSRF cookie does not match PUT argument

so I can’t say for certain that this solved the issue. Will continue to monitor this but in the meantime do you happen to have any other suggestions?

Thanks so much for your help so far!