linkding
linkding copied to clipboard
CSRF verification fails when running linkding behind a proxy such as nginx
Since version 1.15.0 linkding uses Django 4.1, which introduces new restrictions to CSRF handling. This can cause CSRF verification to fail (for example during login) if the app is running behind a proxy and is not properly configured for it.
This needs investigation if the app can provide some default configuration out of the box, otherwise this requires documentation on how to properly configure CSRF when using a proxy.
same here, I'm using apache.
Edit: This has been solved with #349
The README now provides instructions on how to configure Nginx, alternatively a new setting (LD_CSRF_TRUSTED_ORIGINS
) can be used to configure trusted origins which should work regardless of which reverse proxy is used.
~~Workarounds for the moment:~~
- ~~Use version 1.14.0. Docker tag is
sissbruecker/linkding:1.14.0
. You will not miss much, there are no critical fixes in the latest release. You can also downgrade, there are no DB migrations between the two versions.~~ - ~~Configure your domain as trusted origin:~~
- ~~Create a
custom.py
file~~ - ~~Add this line to the file:
CSRF_TRUSTED_ORIGINS = ['https://linkding.domain.com']
, make sure to use the correct protocol (http or https) and domain~~ - ~~Mount the file into the Docker container by adding an additional volume to the Docker run command:
-v ./custom.py:/etc/linkding/siteroot/settings/custom.py
~~ - ~~Create a new container~~
- ~~Create a
- ~~For Nginx, configure the reverse proxy so that it forwards the correct
host
header instead of rewriting it: https://github.com/sissbruecker/linkding/issues/340#issuecomment-1247225877~~
The new CSRF check in Django 4 requires that the values of the host
and origin
request headers match. A valid combination would be for example:
Host: linkding.domain.com
Origin: https://linkding.domain.com
From some testing it seems that Nginx changes the value of the host
request header to whatever is used in the proxy_pass
directive. So if you use proxy_pass http://localhost:9090
, then the host
header is changed to localhost:9090
, which causes the CSRF check to fail. The same does not happen when using Caddy, which just relays the original host
header.
So another solution is to configure Nginx to relay the original host header:
location / {
proxy_pass http://localhost:9090;
proxy_set_header Host $http_host;
}
nginx subpath config,
-
custom.py
method works with latest image. -
proxy_set_header Host $http_host;
option makes no different. -
- with custom.py subpath url will work
-
- without custom.py subpath url will goes to 403 forbidden
Tested forwarding the host
header with a sub path in Nginx, works fine for me. Using a context path doesn't make any difference for the CSRF check, as neither the Host
or Origin
header contain the path, and the proxy_set_header Host $http_host;
directive will also not add the path.
I'm testing with the following docker-compose setup:
version: '3'
services:
linkding:
ports:
- 9090
environment:
LD_CONTEXT_PATH: linkding/
LD_SUPERUSER_NAME: admin
LD_SUPERUSER_PASSWORD: admin
image: sissbruecker/linkding:latest
nginx:
image: nginx:1.22.0
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- '80:80'
depends_on:
- linkding
And this is nginx.conf
:
server {
listen 80;
server_name _;
location /linkding/ {
proxy_pass http://linkding:9090;
proxy_set_header Host $http_host;
}
}
Hi!
Thanks for the project. I tried to set it up with docker and NGinx reverse proxy, but I always get
Verboten (403)
CSRF-Verifizierung fehlgeschlagen. Anfrage abgebrochen.
Mehr Information ist verfügbar mit DEBUG=True.
This is my NGinx config and it does NOT work.
server {
listen 443 ssl;
server_name bookmarks.mydomain.com;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/bookmarks.mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bookmarks.mydomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
set $upstream linkding;
resolver 127.0.0.11 valid=30s;
#proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_pass http://$upstream:9090;
}
}
I usually use proxy_set_header Host $host
for my domains but didn't not work here, so I replaced it with proxy_set_header Host $http_host
as mentioned above, but I face still same problem.
Any help is welcome! Thank you!
For me, it works now. I added proxy_set_header X-Forwarded-Proto $scheme;
after reading https://stackoverflow.com/questions/38391376/nginx-gunicorn-django-1-9-csrf-verification-failed
I have this error when using the API. It makes it impossible to use the extension. Even if I add the internal UUID of the extension to the allowed origins, it now returns:
{"detail":"CSRF Failed: CSRF token missing."}
For some reason, just using a very basic console request works flawlessly and without needing to allow extra origins or to edit nginx config:
$ https example.com/linkding/api/bookmarks/ "Authorization:Token REDACTED" url='https://www.kytta.dev/'
HTTP/1.1 201 Created
[...]
{
...
}
For information, I am using the latest Docker container (haven't tried downgrading yet) running on YunoHost 11.0.9.14 (as Redirect app) with their default Nginx config. I run it on a context path DOMAIN_NAME/linkding/
. The linkding's frontend works flawlessly by the way, I get no errors with CSRF there, only the extension makes problems...
UPDATE: Removing the cookies for SSOwat (SSOwAuthUser=NAME; SSOwAuthHash=HASH; SSOwAuthExpire=TIMESTAMP
) apparently fixes the issue. I guess my problem is relevant for YunoHost users only
UPDATE 2: I gave up and reconfigured the app to use a subdomain. Works like a charm 🤷♂️
I have this error when using the API. It makes it impossible to use the extension. Even if I add the internal UUID of the extension to the allowed origins, it now returns:
{"detail":"CSRF Failed: CSRF token missing."}
That's actually a different error. All reports so far are for POST requests in the UI, where a valid CSRF token is passed, but then verification fails because host and origin don't match. Your error indicates that no token at all was passed, but the app still wants to verify one. Which is weird, because the REST API is configured to use token authentication which does not require a CSRF token. It kind of sounds like either:
- the request for saving the bookmark from the extension did not end up at a REST API endpoint
- or the auth token was stripped from the request, which would cause the API to fall back to session authentication, which requires a CSRF token
Not quite sure what's going on here. I just did a basic test with mis-configured CSRF in Nginx, and I can still add bookmarks through the extension.