CSRF Reverse Proxy: Configuration issue or bug?

What is the problem?

Using nginx as a reverse proxy, when reverse proxying HTTPS on a non-standard port, there is no way to get Octoprint to respond with the appropriate CSRF port.

The main problem seems to be that X-Forwarded-Port is not respected, and when X-Forwarded-Proto is used it overwrites the ports used for CSRF.

What did you already try to solve it?

I've done some pretty extensive research into CSRF and nginx, and followed ideas from Reverse proxy configuration examples

Configurations I have attempted include almost every combination of commenting and uncommenting the below proxy_set_header lines. The below configuration results in the attached screenshot (edited domain).

        server {
                listen 12345 ssl;
                server_name octoprint.domain.com;
                gzip off;
                ssl_certificate /letsencrypt/live/domain.com/fullchain.pem;
                ssl_certificate_key /letsencrypt/live/domain.com/privkey.pem;
                ssl_session_cache shared:SSL:10m;
                ssl_protocols TLSv1.2;
                ssl_ciphers HIGH:!aNULL:!MD5;
                modsecurity on;
                modsecurity_rules_file /etc/nginx/conf/modsec.conf;
                add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
                proxy_buffering off;
                client_max_body_size 10G;

                location / {
                        proxy_pass http://192.168.0.21/;
                        proxy_set_header Host $host;
                        proxy_set_header X-Real-IP $remote_addr;
                        #proxy_set_header X-Scheme $scheme;
                        proxy_set_header X-Forwarded-Proto $scheme;
                        proxy_set_header X-Forwarded-Scheme $scheme;
                        #proxy_set_header X-Forwaded-Host "octoprint.domain.com";
                        #proxy_set_header X-Forwaded-Host "octoprint.domain.com:12345";
                        proxy_set_header X-Forwaded-Host $host;
                        #proxy_set_header X-Forwarded-Port 12345;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_http_version 1.1;
                        proxy_set_header Upgrade $http_upgrade;
                        proxy_set_header Connection $http_connection;
                        #proxy_set_header Authorization $http_authorization;
                        #proxy_set_header X-Forwarded-Ssl on;
                        #proxy_pass_header X-CSRFToken;
                }
        }

Systeminfo Bundle

browser.user_agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0 connectivity.connection_check: 1.1.1.1:53 connectivity.connection_ok: True connectivity.enabled: True connectivity.online: True connectivity.resolution_check: octoprint.org connectivity.resolution_ok: True env.hardware.cores: 4 env.hardware.freq: 1200.0 env.hardware.ram: 912883712 env.os.bits: 32 env.os.id: linux env.os.platform: linux env.plugins.pi_support.model: Raspberry Pi 3 Model B Rev 1.2 env.plugins.pi_support.octopi_version: 1.0.0 env.plugins.pi_support.octopiuptodate_build: 1.0.0-1.8.6-20230222095002 env.plugins.pi_support.throttle_check_enabled: True env.plugins.pi_support.throttle_check_functional: True env.plugins.pi_support.throttle_state: 0x50000 env.python.pip: 22.3 env.python.version: 3.9.2 env.python.virtualenv: True octoprint.last_safe_mode.date: unknown octoprint.last_safe_mode.reason: unknown octoprint.safe_mode: False octoprint.version: 1.8.6 systeminfo.generated: 2023-02-23T20:53:06Z systeminfo.generator: zipapi

Hello @fmstrat !

Can you please share the systeminfo bundle.

Oh yea, totally misinterpreted the bundle vs file. Since gcode filenames and usernames are identifiable in the logs, I'm going to sanitize first.

@Ewald_Ikemann Here you go, thanks!
octoprint-systeminfo-sanitized-20230223155306.zip (45.2 KB)

Sorry, for what?

Oh I'm just over-sensitized. Given my gcode names are pretty specific to things I do, and filenames and usernames are in the octoprint.log, I cleaned them out of the log.

Your system looks like it's an OctoPi install, which uses haproxy for OctoPrint and not nginx, and the haproxy.log that is included in the system info bundle seems to indicate that that is still running on the Pi when you are trying to access it.

I wonder if there is some conflict between the two reverse proxies going on and nginx & haproxy are not passing everything to OctoPrint. It sounds like it might be setup to go from 'your domain', nginx -> <octopi port 80> (haproxy) -> OctoPrint? Is that correct?

That is spot on. Guess I should look at haproxy config and point nginx to the same place? Any idea of octopi needs a firewall adjustment for that? Thanks!

Steps further, but no luck. It appears to be OctoPrint itself that's generating the CSRF root. Steps I've taken:

  • Edited /etc/systemd/system/octoprint.service to change the host to 0.0.0.0
  • Restarted with systemctl (Including a daemon-reload)
  • I can now login at http://192.168.0.21:5000
  • Pointed my nginx proxy to http://192.168.0.21:5000.
  • Went to https://octoprint.domain.com:12345 and still get a 400 when logging in. I recorded the request/response in nginx and get the following. The csrf_token_P443 seems to be the root of the issue.
        "request": {
            "method": "POST",
            "http_version": 2.0,
            "uri": "/api/login",
            "headers": {
                "origin": "https://octoprint.domain.com:12345",
                "dnt": "1",
                "cache-control": "no-cache",
                "accept": "application/json, text/javascript, */*; q=0.01",
                "x-requested-with": "XMLHttpRequest",
                "content-type": "application/json; charset=UTF-8",
                "accept-encoding": "gzip, deflate, br",
                "cookie": "csrf_token_P443=<redacted>",
                "content-length": "57",
                "accept-language": "en-US,en;q=0.8,fr;q=0.7,ja;q=0.5,de;q=0.3,es;q=0.2",
                "te": "trailers",
                "user-agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0",
                "sec-fetch-site": "same-origin",
                "host": "octoprint.domain.com:12345",
                "referer": "https://octoprint.domain.com:12345/login/?redirect=%2F%3F&permissions=STATUS%2CSETTINGS_READ",
                "sec-fetch-dest": "empty",
                "sec-fetch-mode": "cors"
            }
        },
        "response": {
            "body": "",
            "http_code": 400,
            "headers": {
                "Vary": "Cookie",
                "Pragma": "no-cache",
                "Cache-Control": "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0",
                "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
                "Connection": "close",
                "Content-Type": "application/json",
                "X-Clacks-Overhead": "GNU Terry Pratchett",
                "Expires": "-1",
                "Content-Length": "35",
                "Date": "Fri, 24 Feb 2023 17:17:30 GMT",
                "Server-Timing": "app;dur=13",
                "Set-Cookie": "session_P443=.eJxl<redacted>; HttpOnly; Path=/; SameSite=Lax",
                "Server": "nginx/1.20.2",
                "X-Robots-Tag": "noindex, nofollow, noimageindex",
                "X-Frame-Options": "sameorigin",
                "X-Content-Type-Options": "nosniff"
            }
        },

Ok, this is super-weird. Started looking at the OctoPrint CSRF code, and saw the comparison between the csrf_token cookie and the X-CSRF-Token header.

When submitting the login form through haproxy at http://192.168.0.21, the X-CSRF-Token header exists in the request headers of my browser.

When submitting the login form through nginx at https://octoprint.domain.com:12345, the X-CSRF-Token header is not sent by my browser.

How on earth is the proxy itself telling the browser not to set the header in the request?

Hrm, perhaps this is a bug:

This is a theory, but I believe in getParsedBaseUrl it's filtering out the custom port, and thus port != url.port, so since url.protocol === "https:" the port for the cookie suffix is getting set to 443.

Is baseurl still used? Where does this get set:

That would cause the port to not be relayed down the line for the cookie suffix.

90% sure this is it now. baseurl is not in existance, so getParsedBaseUrl is returning back "" and since it's an https scheme, 443 is assigned.

getCookieSuffix is used in getCookieName is used in getCookie is used in getRequestHeaders which would make csrfToken == null here:

I'll report this as an issue unless someone says otherwise.

EDIT: CSRF fails to function on non-standard port (non 443) · Issue #4740 · OctoPrint/OctoPrint · GitHub

1 Like

I was able to solve this and submitted a PR: Remove custom port from CSRF cookie suffix by Fmstrat · Pull Request #4741 · OctoPrint/OctoPrint · GitHub

EDIT: Was able to solve after additional troubleshooting with the below configuration change in nginx:

proxy_set_header X-Forwarded-Host $host:12345;
2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.