From 1c3651e91dba50a11276be5f745bd3f8712df22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Wed, 5 Oct 2022 10:01:44 +0200 Subject: [PATCH] Add setting and documentation for fixing CSRF errors (#349) * Add documentation and setting for solving CSRF errors * Improve proxy setup docs * Link to reverse proxy documentation * Fix link --- .env.sample | 5 +++- README.md | 41 +++++++++++++++++++++++++++++ bookmarks/tests/test_app_options.py | 29 ++++++++++++++++++++ docs/Options.md | 15 +++++++++++ docs/troubleshooting.md | 8 ++++++ siteroot/settings/base.py | 6 +++++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 bookmarks/tests/test_app_options.py diff --git a/.env.sample b/.env.sample index 1daaccd..0625618 100644 --- a/.env.sample +++ b/.env.sample @@ -4,10 +4,10 @@ LD_CONTAINER_NAME=linkding LD_HOST_PORT=9090 # Directory on the host system that should be mounted as data dir into the Docker container LD_HOST_DATA_DIR=./data + # Can be used to run linkding under a context path, for example: linkding/ # Must end with a slash `/` LD_CONTEXT_PATH= - # Username of the initial superuser to create, leave empty to not create one LD_SUPERUSER_NAME= # Password for the initial superuser, leave empty to disable credentials authentication and rely on proxy authentication instead @@ -24,3 +24,6 @@ LD_AUTH_PROXY_USERNAME_HEADER= # The URL that linkding should redirect to after a logout, when using an auth proxy # See docs/Options.md for more details LD_AUTH_PROXY_LOGOUT_URL= +# List of trusted origins from which to accept POST requests +# See docs/Options.md for more details +LD_CSRF_TRUSTED_ORIGINS= diff --git a/README.md b/README.md index fae13f2..12f0789 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - [Using Docker](#using-docker) - [Using Docker Compose](#using-docker-compose) - [User Setup](#user-setup) + - [Reverse Proxy Setup](#reverse-proxy-setup) - [Managed Hosting Options](#managed-hosting-options) - [Documentation](#documentation) - [Browser Extension](#browser-extension) @@ -96,6 +97,46 @@ docker-compose exec linkding python manage.py createsuperuser --username=joe --e The command will prompt you for a secure password. After the command has completed you can start using the application by logging into the UI with your credentials. +### Reverse Proxy Setup + +When using a reverse proxy, such as Nginx or Apache, you may need to configure your proxy to correctly forward the `Host` header to linkding, otherwise certain requests, such as login, might fail. + +
+Apache + +Not tested yet. +If you figure out a working setup, feel free to contribute it here. + +In the meanwhile, use the [`LD_CSRF_TRUSTED_ORIGINS` option](docs/Options.md#LD_CSRF_TRUSTED_ORIGINS). + +
+ +
+Caddy 2 + +Caddy does not change the headers by default, and should not need any further configuration. + +If you still run into CSRF issues, please check out the [`LD_CSRF_TRUSTED_ORIGINS` option](docs/Options.md#LD_CSRF_TRUSTED_ORIGINS). + +
+ +
+Nginx + +Nginx by default rewrites the `Host` header to whatever URL is used in the `proxy_pass` directive. +To forward the correct headers to linkding, add the following directives to the location block of your Nginx config: +``` +location /linkding { + ... + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +
+ +Instead of configuring header forwarding in your proxy, you can also configure the URL from which you want to access your linkding instance with the [`LD_CSRF_TRUSTED_ORIGINS` option](docs/Options.md#LD_CSRF_TRUSTED_ORIGINS). + ### Managed Hosting Options Self-hosting web applications on your own hardware (unfortunately) still requires a lot of technical know-how, and commitment to maintenance, with regard to keeping everything up-to-date and secure. This can be a huge entry barrier for people who are interested in self-hosting linkding, but lack the technical knowledge to do so. This section is intended to provide alternatives in form of managed hosting solutions. Note that these options are usually commercial offerings, that require paying a (usually monthly) fee for the convenience of being managed by another party. The technical knowledge required to make use of individual options is going to vary, and no guarantees can be made that every option is accessible for everyone. That being said, I hope this section helps in making the application accessible to a wider audience. diff --git a/bookmarks/tests/test_app_options.py b/bookmarks/tests/test_app_options.py new file mode 100644 index 0000000..f359184 --- /dev/null +++ b/bookmarks/tests/test_app_options.py @@ -0,0 +1,29 @@ +import importlib +import os +from unittest import mock + +from django.test import TestCase + + +class AppOptionsTestCase(TestCase): + def setUp(self) -> None: + self.settings_module = importlib.import_module('siteroot.settings.base') + + def test_empty_csrf_trusted_origins(self): + module = importlib.reload(self.settings_module) + + self.assertFalse(hasattr(module, 'CSRF_TRUSTED_ORIGINS')) + + @mock.patch.dict(os.environ, {'LD_CSRF_TRUSTED_ORIGINS': 'https://linkding.example.com'}) + def test_single_csrf_trusted_origin(self): + module = importlib.reload(self.settings_module) + + self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS')) + self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com']) + + @mock.patch.dict(os.environ, {'LD_CSRF_TRUSTED_ORIGINS': 'https://linkding.example.com,http://linkding.example.com'}) + def test_multiple_csrf_trusted_origin(self): + module = importlib.reload(self.settings_module) + + self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS')) + self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com', 'http://linkding.example.com']) diff --git a/docs/Options.md b/docs/Options.md index 705c00e..a83b58b 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -93,3 +93,18 @@ For example, for Authelia, which passes the `Remote-User` HTTP header, the `LD_A - `LD_AUTH_PROXY_LOGOUT_URL` - The URL that linkding should redirect to after a logout. By default, the logout redirects to the login URL, which means the user will be automatically authenticated again. Instead, you might want to configure the logout URL of the auth proxy here. + +### `LD_CSRF_TRUSTED_ORIGINS` + +Values: `String` | Default = None + +List of trusted origins / host names to allow for `POST` requests, for example when logging in, or saving bookmarks. +For these type of requests, the `Origin` header must match the `Host` header, otherwise the request will fail with a `403` status code, and the message `CSRF verification failed.` + +This option allows to declare a list of trusted origins that will be accepted even if the headers do not match. This can be the case when using a reverse proxy that rewrites the `Host` header, such as Nginx. + +For example, to allow requests to https://linkding.mydomain.com, configure the setting to `https://linkding.mydomain.com`. +Note that the setting **must** include the correct protocol (`https` or `http`), and **must not** include the application / context path. +Multiple origins can be specified by separating them with a comma (`,`). + +This setting is adopted from the Django framework used by linkding, more information on the setting is available in the [Django documentation](https://docs.djangoproject.com/en/4.0/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1ab4953..0c084dd 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,13 @@ # Troubleshooting +## Login fails with `403 CSRF verfication failed` + +This can be the case when using a reverse proxy that rewrites the `Host` header, such as Nginx. +Since linkding version 1.15, the application includes a CSRF check that verifies that the `Origin` request header matches the `Host` header. +If the `Host` header is modified by the reverse proxy then this check fails. + +To fix this, check the [reverse proxy setup documentation](../README.md#reverse-proxy-setup) on how to configure header forwarding for your proxy server, or alternatively configure the [`LD_CSRF_TRUSTED_ORIGINS` option](Options.md#LD_CSRF_TRUSTED_ORIGINS) to the URL from which you are accessing your linkding instance. + ## Import fails with `502 Bad Gateway` The default timeout for requests is 60 seconds, after which the application server will cancel the request and return the above error. diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py index 7462601..11b59e0 100644 --- a/siteroot/settings/base.py +++ b/siteroot/settings/base.py @@ -198,3 +198,9 @@ if LD_ENABLE_AUTH_PROXY: # Configure logout URL if LD_AUTH_PROXY_LOGOUT_URL: LOGOUT_REDIRECT_URL = LD_AUTH_PROXY_LOGOUT_URL + +# CSRF trusted origins +trusted_origins = os.getenv('LD_CSRF_TRUSTED_ORIGINS', '') +if trusted_origins: + CSRF_TRUSTED_ORIGINS = trusted_origins.split(',') +