Repository: miniflux/v2 (language: go)

Issue:
Add CONTENT_SECURITY_POLICY
Added `CONTENT_SECURITY_POLICY` environment variable to be able to set custom domain for `Content-Security-Policy` meta tag.

Resolves #748

Do you follow the guidelines?

- [x] I have tested my changes
- [x] I read this document: https://miniflux.app/faq.html#pull-request

Can't load external fonts inside custom CSS
When I tried to load Google Fonts inside Miniflux's custom CSS, I was met with this error:

> Refused to load the stylesheet 'https://fonts.googleapis.com/css2?family=Roboto&display=swap' because it violates the following Content Security Policy directive: "default-src 'self'". Note that 'style-src-elem' was not explicitly set, so 'default-src' is used as a fallback.

And of course, the external fonts from Google Fonts didn't load. Is this as expected or by design? Currently I have to load my fonts with a user styles manager like Stylus for Chrome as a temporary solution.


Discussion / hints:
Duplicate of #1987
I'm interested as well, I have been trying to configure CSP via Cloudron for a Miniflux hosted app without success until I realize this is not supported in Miniflux.
This behaviour is expected. CSP directives are there for security reasons.
> This behaviour is expected. CSP directives are there for security reasons.

All right, thanks for the clarification.
If one wants to trust google fonts anyway, is there a supported way to do so today other than monkey patching the repository and changing the CSP in the source code?
> If one wants to trust google fonts anyway, is there a supported way to do so today other than monkey patching the repository and changing the CSP in the source code?

The actual implementation could be changed to make the CSP configurable.
In this case, would you like to reopen this issue by repurposing it to make the CSP configurable?

Fix patch:
diff --git a/client/model.go b/client/model.go
index f0583ae486c..0a6cd4d1016 100644
--- a/client/model.go
+++ b/client/model.go
@@ -45,6 +45,7 @@ type User struct {
 	MediaPlaybackRate      float64    `json:"media_playback_rate"`
 	BlockFilterEntryRules  string     `json:"block_filter_entry_rules"`
 	KeepFilterEntryRules   string     `json:"keep_filter_entry_rules"`
+	ExternalFontHosts      string     `json:"external_font_hosts"`
 }
 
 func (u User) String() string {
@@ -88,6 +89,7 @@ type UserModificationRequest struct {
 	MediaPlaybackRate      *float64 `json:"media_playback_rate"`
 	BlockFilterEntryRules  *string  `json:"block_filter_entry_rules"`
 	KeepFilterEntryRules   *string  `json:"keep_filter_entry_rules"`
+	ExternalFontHosts      *string  `json:"external_font_hosts"`
 }
 
 // Users represents a list of users.
diff --git a/internal/database/migrations.go b/internal/database/migrations.go
index 2c0939dabe6..2c7ea8b2838 100644
--- a/internal/database/migrations.go
+++ b/internal/database/migrations.go
@@ -947,4 +947,9 @@ var migrations = []func(tx *sql.Tx) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `ALTER TABLE users ADD COLUMN external_font_hosts text not null default '';`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }
diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json
index 82c508ee88e..370a9f3bfca 100644
--- a/internal/locale/translations/de_DE.json
+++ b/internal/locale/translations/de_DE.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Geste zum Navigieren zwischen Einträgen",
     "form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
     "form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
-    "form.prefs.label.custom_js": "Benutzerdefiniertes JS",
+    "form.prefs.label.custom_js": "Benutzerdefiniertes JavaScript",
     "form.prefs.label.entry_order": "Artikel-Sortierspalte",
     "form.prefs.label.default_home_page": "Standard-Startseite",
     "form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen",
     "form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "Externe Schriftarten-Hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "OPML Datei",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json
index 5f80554632f..1585128cdf1 100644
--- a/internal/locale/translations/el_EL.json
+++ b/internal/locale/translations/el_EL.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων",
     "form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
     "form.prefs.label.custom_css": "Προσαρμοσμένο CSS",
-    "form.prefs.label.custom_js": "Προσαρμοσμένο JS",
+    "form.prefs.label.custom_js": "Προσαρμοσμένο JavaScript",
     "form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου",
     "form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
     "form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "Αρχείο OPML",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json
index 8c7f65ae0b8..eaf4d1e89d1 100644
--- a/internal/locale/translations/en_US.json
+++ b/internal/locale/translations/en_US.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Gesture to navigate between entries",
     "form.prefs.label.show_reading_time": "Show estimated reading time for entries",
     "form.prefs.label.custom_css": "Custom CSS",
-    "form.prefs.label.custom_js": "Custom JS",
+    "form.prefs.label.custom_js": "Custom JavaScript",
     "form.prefs.label.entry_order": "Entry sorting column",
     "form.prefs.label.default_home_page": "Default home page",
     "form.prefs.label.categories_sorting_order": "Categories sorting",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "OPML file",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json
index 1e76897eeb2..51cd0f230aa 100644
--- a/internal/locale/translations/es_ES.json
+++ b/internal/locale/translations/es_ES.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Gesto para navegar entre entradas",
     "form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
     "form.prefs.label.custom_css": "CSS personalizado",
-    "form.prefs.label.custom_js": "JS personalizado",
+    "form.prefs.label.custom_js": "JavaScript personalizado",
     "form.prefs.label.entry_order": "Columna de clasificación de artículos",
     "form.prefs.label.default_home_page": "Página de inicio por defecto",
     "form.prefs.label.categories_sorting_order": "Clasificación por categorías",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Ajustes de la autentificación",
     "form.prefs.fieldset.reader_settings": "Ajustes del lector",
     "form.prefs.fieldset.global_feed_settings": "Ajustes globales del feed",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "Archivo OPML",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Guardar artículos en Betula",
diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json
index e543dfd3819..05945de8690 100644
--- a/internal/locale/translations/fi_FI.json
+++ b/internal/locale/translations/fi_FI.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Ele siirtyäksesi merkintöjen välillä",
     "form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
     "form.prefs.label.custom_css": "Mukautettu CSS",
-    "form.prefs.label.custom_js": "Mukautettu JS",
+    "form.prefs.label.custom_js": "Mukautettu JavaScript",
     "form.prefs.label.entry_order": "Lajittele sarakkeen mukaan",
     "form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
     "form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "OPML-tiedosto",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json
index d54e689c97a..8fdf4f2e9ab 100644
--- a/internal/locale/translations/fr_FR.json
+++ b/internal/locale/translations/fr_FR.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Geste pour naviguer entre les entrées",
     "form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
     "form.prefs.label.custom_css": "Feuille de style personnalisée",
-    "form.prefs.label.custom_js": "Script personnalisée",
+    "form.prefs.label.custom_js": "Code JavaScript personnalisé",
     "form.prefs.label.entry_order": "Colonne de tri des entrées",
     "form.prefs.label.default_home_page": "Page d'accueil par défaut",
     "form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Paramètres d'authentification",
     "form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
     "form.prefs.fieldset.global_feed_settings": "Paramètres globaux des abonnements",
+    "form.prefs.label.external_font_hosts": "Polices externes autorisées",
+    "form.prefs.help.external_font_hosts": "Liste de domaine externes autorisés, séparés par des espaces. Par exemple : « fonts.gstatic.com fonts.googleapis.com ».",
+    "error.settings_invalid_domain_list": "Liste de domaines invalide. Veuillez fournir une liste de domaines séparés par des espaces.",
     "form.import.label.file": "Fichier OPML",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Sauvegarder les entrées vers Betula",
diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json
index 026f3e4c08a..746eed9be5f 100644
--- a/internal/locale/translations/hi_IN.json
+++ b/internal/locale/translations/hi_IN.json
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "ओपीएमएल फ़ाइल",
     "form.import.label.url": "यूआरएल",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json
index 75cef1fc46a..e92516e05e0 100644
--- a/internal/locale/translations/id_ID.json
+++ b/internal/locale/translations/id_ID.json
@@ -381,7 +381,7 @@
     "form.prefs.label.gesture_nav": "Isyarat untuk menavigasi antar entri",
     "form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
     "form.prefs.label.custom_css": "Modifikasi CSS",
-    "form.prefs.label.custom_js": "Modifikasi JS",
+    "form.prefs.label.custom_js": "Modifikasi JavaScript",
     "form.prefs.label.entry_order": "Pengurutan Kolom Entri",
     "form.prefs.label.default_home_page": "Beranda Baku",
     "form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
@@ -393,6 +393,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "Berkas OPML",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json
index 930b80ba944..d754f5e50d3 100644
--- a/internal/locale/translations/it_IT.json
+++ b/internal/locale/translations/it_IT.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Gesto per navigare tra le voci",
     "form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
     "form.prefs.label.custom_css": "CSS personalizzati",
-    "form.prefs.label.custom_js": "JS personalizzati",
+    "form.prefs.label.custom_js": "JavaScript personalizzati",
     "form.prefs.label.entry_order": "Colonna di ordinamento delle voci",
     "form.prefs.label.default_home_page": "Pagina iniziale predefinita",
     "form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "File OPML",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json
index ad4cceeee2c..0b0a8f5f9c0 100644
--- a/internal/locale/translations/ja_JP.json
+++ b/internal/locale/translations/ja_JP.json
@@ -381,7 +381,7 @@
     "form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー",
     "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
     "form.prefs.label.custom_css": "カスタム CSS",
-    "form.prefs.label.custom_js": "カスタム JS",
+    "form.prefs.label.custom_js": "カスタム JavaScript",
     "form.prefs.label.entry_order": "記事の表示順の基準",
     "form.prefs.label.default_home_page": "デフォルトのトップページ",
     "form.prefs.label.categories_sorting_order": "カテゴリの表示順",
@@ -393,6 +393,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "OPML ファイル",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json
index 0e278c1a1ac..fc7ef956aef 100644
--- a/internal/locale/translations/nl_NL.json
+++ b/internal/locale/translations/nl_NL.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Gebaar om tussen artikelen te navigeren",
     "form.prefs.label.show_reading_time": "Toon geschatte leestijd van artikelen",
     "form.prefs.label.custom_css": "Aangepaste CSS",
-    "form.prefs.label.custom_js": "Aangepaste JS",
+    "form.prefs.label.custom_js": "Aangepaste JavaScript",
     "form.prefs.label.entry_order": "Artikelen sorteren",
     "form.prefs.label.default_home_page": "Startpagina",
     "form.prefs.label.categories_sorting_order": "Volgorde categorieën",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Authenticatie Instellingen",
     "form.prefs.fieldset.reader_settings": "Lees Instellingen",
     "form.prefs.fieldset.global_feed_settings": "Globale Feed Instellingen",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "OPML-bestand",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Artikelen opslaan in Betula",
diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json
index 0dd6ae04bd5..4e60d0279dd 100644
--- a/internal/locale/translations/pl_PL.json
+++ b/internal/locale/translations/pl_PL.json
@@ -401,7 +401,7 @@
     "form.prefs.select.tap": "Podwójne wciśnięcie",
     "form.prefs.select.swipe": "Trzepnąć",
     "form.prefs.label.custom_css": "Niestandardowy CSS",
-    "form.prefs.label.custom_js": "Niestandardowy JS",
+    "form.prefs.label.custom_js": "Niestandardowy JavaScript",
     "form.prefs.label.entry_order": "Kolumna sortowania wpisów",
     "form.prefs.label.default_home_page": "Domyślna strona główna",
     "form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
@@ -413,6 +413,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "Plik OPML",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json
index 0e10d728498..a1dc2b12467 100644
--- a/internal/locale/translations/pt_BR.json
+++ b/internal/locale/translations/pt_BR.json
@@ -391,7 +391,7 @@
     "form.prefs.label.gesture_nav": "Gesto para navegar entre as entradas",
     "form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
     "form.prefs.label.custom_css": "CSS customizado",
-    "form.prefs.label.custom_js": "JS customizado",
+    "form.prefs.label.custom_js": "JavaScript customizado",
     "form.prefs.label.entry_order": "Coluna de Ordenação de Entrada",
     "form.prefs.label.default_home_page": "Página inicial predefinida",
     "form.prefs.label.categories_sorting_order": "Classificação das categorias",
@@ -403,6 +403,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "Arquivo OPML",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json
index a040d8797c0..4c6b26bc7de 100644
--- a/internal/locale/translations/ru_RU.json
+++ b/internal/locale/translations/ru_RU.json
@@ -401,7 +401,7 @@
     "form.prefs.label.gesture_nav": "Жест для перехода между статьями",
     "form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
     "form.prefs.label.custom_css": "Пользовательский CSS",
-    "form.prefs.label.custom_js": "Пользовательский JS",
+    "form.prefs.label.custom_js": "Пользовательский JavaScript",
     "form.prefs.label.entry_order": "Столбец сортировки статей",
     "form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
     "form.prefs.label.categories_sorting_order": "Сортировка категорий",
@@ -413,6 +413,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "OPML файл",
     "form.import.label.url": "Ссылка",
     "form.integration.betula_activate": "Сохранять статьи в Бетулу",
diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json
index c9f55c5dd82..91ac11682be 100644
--- a/internal/locale/translations/tr_TR.json
+++ b/internal/locale/translations/tr_TR.json
@@ -293,7 +293,7 @@
   "form.prefs.label.categories_sorting_order": "Kategori sıralaması",
   "form.prefs.label.cjk_reading_speed": "Çince, Korece ve Japonca için okuma hızı (dakika başına karakter)",
   "form.prefs.label.custom_css": "Özel CSS",
-  "form.prefs.label.custom_js": "Özel JS",
+  "form.prefs.label.custom_js": "Özel JavaScript",
   "form.prefs.label.default_home_page": "Varsayılan ana sayfa",
   "form.prefs.label.default_reading_speed": "Diğer diller için okuma hızı (dakika başına kelime)",
   "form.prefs.label.display_mode": "Progressive Web App (PWA) görüntüleme modu",
@@ -325,6 +325,9 @@
   "form.prefs.select.swipe": "Kaydırma",
   "form.prefs.select.tap": "Çift dokunma",
   "form.prefs.select.unread_count": "Okunmamış sayısı",
+  "form.prefs.label.external_font_hosts": "External font hosts",
+  "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+  "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
   "form.submit.loading": "Yükleniyor...",
   "form.submit.saving": "Kaydediliyor...",
   "form.user.label.admin": "Yönetici",
diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json
index 91027cb77b3..7ca464184c6 100644
--- a/internal/locale/translations/uk_UA.json
+++ b/internal/locale/translations/uk_UA.json
@@ -401,7 +401,7 @@
     "form.prefs.label.gesture_nav": "Жест для переходу між записами",
     "form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
     "form.prefs.label.custom_css": "Спеціальний CSS",
-    "form.prefs.label.custom_js": "Спеціальний JS",
+    "form.prefs.label.custom_js": "Спеціальний JavaScript",
     "form.prefs.label.entry_order": "Стовпець сортування записів",
     "form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
     "form.prefs.label.categories_sorting_order": "Сортування за категоріями",
@@ -413,6 +413,9 @@
     "form.prefs.fieldset.authentication_settings": "Authentication Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "Файл OPML",
     "form.import.label.url": "URL-адреса",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json
index 2942847e517..8309e7a752d 100644
--- a/internal/locale/translations/zh_CN.json
+++ b/internal/locale/translations/zh_CN.json
@@ -381,7 +381,7 @@
     "form.prefs.label.gesture_nav": "在条目之间导航的手势",
     "form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
     "form.prefs.label.custom_css": "自定义 CSS",
-    "form.prefs.label.custom_js": "自定义 JS",
+    "form.prefs.label.custom_js": "自定义 JavaScript",
     "form.prefs.label.entry_order": "文章排序依据",
     "form.prefs.label.default_home_page": "默认主页",
     "form.prefs.label.categories_sorting_order": "分类排序",
@@ -393,6 +393,9 @@
     "form.prefs.fieldset.authentication_settings": "用户认证设置",
     "form.prefs.fieldset.reader_settings": "阅读器设置",
     "form.prefs.fieldset.global_feed_settings": "全局订阅源设置",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "OPML 文件",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "保存文章到 Betula",
diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json
index b9505aac4c3..3c7c46004d0 100644
--- a/internal/locale/translations/zh_TW.json
+++ b/internal/locale/translations/zh_TW.json
@@ -381,7 +381,7 @@
     "form.prefs.label.gesture_nav": "在條目之間導航的手勢",
     "form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
     "form.prefs.label.custom_css": "自定義 CSS",
-    "form.prefs.label.custom_js": "自定義 JS",
+    "form.prefs.label.custom_js": "自定義 JavaScript",
     "form.prefs.label.entry_order": "文章排序依據",
     "form.prefs.label.default_home_page": "預設主頁",
     "form.prefs.label.categories_sorting_order": "分類排序",
@@ -393,6 +393,9 @@
     "form.prefs.fieldset.authentication_settings": "使用者認證設定",
     "form.prefs.fieldset.reader_settings": "閱讀器設定",
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
+    "form.prefs.label.external_font_hosts": "External font hosts",
+    "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
     "form.import.label.file": "OPML 檔案",
     "form.import.label.url": "URL",
     "form.integration.betula_activate": "Save entries to Betula",
diff --git a/internal/model/user.go b/internal/model/user.go
index ba14b99da19..ad070904e55 100644
--- a/internal/model/user.go
+++ b/internal/model/user.go
@@ -22,6 +22,7 @@ type User struct {
 	EntryOrder                      string     `json:"entry_sorting_order"`
 	Stylesheet                      string     `json:"stylesheet"`
 	CustomJS                        string     `json:"custom_js"`
+	ExternalFontHosts               string     `json:"external_font_hosts"`
 	GoogleID                        string     `json:"google_id"`
 	OpenIDConnectID                 string     `json:"openid_connect_id"`
 	EntriesPerPage                  int        `json:"entries_per_page"`
@@ -62,6 +63,7 @@ type UserModificationRequest struct {
 	EntryOrder                      *string  `json:"entry_sorting_order"`
 	Stylesheet                      *string  `json:"stylesheet"`
 	CustomJS                        *string  `json:"custom_js"`
+	ExternalFontHosts               *string  `json:"external_font_hosts"`
 	GoogleID                        *string  `json:"google_id"`
 	OpenIDConnectID                 *string  `json:"openid_connect_id"`
 	EntriesPerPage                  *int     `json:"entries_per_page"`
@@ -124,6 +126,10 @@ func (u *UserModificationRequest) Patch(user *User) {
 		user.CustomJS = *u.CustomJS
 	}
 
+	if u.ExternalFontHosts != nil {
+		user.ExternalFontHosts = *u.ExternalFontHosts
+	}
+
 	if u.GoogleID != nil {
 		user.GoogleID = *u.GoogleID
 	}
diff --git a/internal/storage/user.go b/internal/storage/user.go
index 43baf0e1d54..39a019ca51c 100644
--- a/internal/storage/user.go
+++ b/internal/storage/user.go
@@ -84,6 +84,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
 			gesture_nav,
 			stylesheet,
 			custom_js,
+			external_font_hosts,
 			google_id,
 			openid_connect_id,
 			display_mode,
@@ -126,6 +127,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
 		&user.GestureNav,
 		&user.Stylesheet,
 		&user.CustomJS,
+		&user.ExternalFontHosts,
 		&user.GoogleID,
 		&user.OpenIDConnectID,
 		&user.DisplayMode,
@@ -165,6 +167,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
 
 // UpdateUser updates a user.
 func (s *Storage) UpdateUser(user *model.User) error {
+	user.ExternalFontHosts = strings.TrimSpace(user.ExternalFontHosts)
+
 	if user.Password != "" {
 		hashedPassword, err := crypto.HashPassword(user.Password)
 		if err != nil {
@@ -187,21 +191,22 @@ func (s *Storage) UpdateUser(user *model.User) error {
 				gesture_nav=$12,
 				stylesheet=$13,
 				custom_js=$14,
-				google_id=$15,
-				openid_connect_id=$16,
-				display_mode=$17,
-				entry_order=$18,
-				default_reading_speed=$19,
-				cjk_reading_speed=$20,
-				default_home_page=$21,
-				categories_sorting_order=$22,
-				mark_read_on_view=$23,
-				mark_read_on_media_player_completion=$24,
-				media_playback_rate=$25,
-				block_filter_entry_rules=$26,
-				keep_filter_entry_rules=$27
+				external_font_hosts=$15,
+				google_id=$16,
+				openid_connect_id=$167,
+				display_mode=$18,
+				entry_order=$19,
+				default_reading_speed=$20,
+				cjk_reading_speed=$21,
+				default_home_page=$22,
+				categories_sorting_order=$23,
+				mark_read_on_view=$24,
+				mark_read_on_media_player_completion=$25,
+				media_playback_rate=$26,
+				block_filter_entry_rules=$27,
+				keep_filter_entry_rules=$28
 			WHERE
-				id=$28
+				id=$29
 		`
 
 		_, err = s.db.Exec(
@@ -220,6 +225,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
 			user.GestureNav,
 			user.Stylesheet,
 			user.CustomJS,
+			user.ExternalFontHosts,
 			user.GoogleID,
 			user.OpenIDConnectID,
 			user.DisplayMode,
@@ -254,21 +260,22 @@ func (s *Storage) UpdateUser(user *model.User) error {
 				gesture_nav=$11,
 				stylesheet=$12,
 				custom_js=$13,
-				google_id=$14,
-				openid_connect_id=$15,
-				display_mode=$16,
-				entry_order=$17,
-				default_reading_speed=$18,
-				cjk_reading_speed=$19,
-				default_home_page=$20,
-				categories_sorting_order=$21,
-				mark_read_on_view=$22,
-				mark_read_on_media_player_completion=$23,
-				media_playback_rate=$24,
-				block_filter_entry_rules=$25,
-				keep_filter_entry_rules=$26
+				external_font_hosts=$14,
+				google_id=$15,
+				openid_connect_id=$16,
+				display_mode=$17,
+				entry_order=$18,
+				default_reading_speed=$19,
+				cjk_reading_speed=$20,
+				default_home_page=$21,
+				categories_sorting_order=$22,
+				mark_read_on_view=$23,
+				mark_read_on_media_player_completion=$24,
+				media_playback_rate=$25,
+				block_filter_entry_rules=$26,
+				keep_filter_entry_rules=$27
 			WHERE
-				id=$27
+				id=$28
 		`
 
 		_, err := s.db.Exec(
@@ -286,6 +293,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
 			user.GestureNav,
 			user.Stylesheet,
 			user.CustomJS,
+			user.ExternalFontHosts,
 			user.GoogleID,
 			user.OpenIDConnectID,
 			user.DisplayMode,
@@ -339,6 +347,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
 			last_login_at,
 			stylesheet,
 			custom_js,
+			external_font_hosts,
 			google_id,
 			openid_connect_id,
 			display_mode,
@@ -379,6 +388,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
 			last_login_at,
 			stylesheet,
 			custom_js,
+			external_font_hosts,
 			google_id,
 			openid_connect_id,
 			display_mode,
@@ -419,6 +429,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
 			last_login_at,
 			stylesheet,
 			custom_js,
+			external_font_hosts,
 			google_id,
 			openid_connect_id,
 			display_mode,
@@ -466,6 +477,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
 			u.last_login_at,
 			u.stylesheet,
 			u.custom_js,
+			u.external_font_hosts,
 			u.google_id,
 			u.openid_connect_id,
 			u.display_mode,
@@ -507,6 +519,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
 		&user.LastLoginAt,
 		&user.Stylesheet,
 		&user.CustomJS,
+		&user.ExternalFontHosts,
 		&user.GoogleID,
 		&user.OpenIDConnectID,
 		&user.DisplayMode,
@@ -620,6 +633,7 @@ func (s *Storage) Users() (model.Users, error) {
 			last_login_at,
 			stylesheet,
 			custom_js,
+			external_font_hosts,
 			google_id,
 			openid_connect_id,
 			display_mode,
@@ -662,6 +676,7 @@ func (s *Storage) Users() (model.Users, error) {
 			&user.LastLoginAt,
 			&user.Stylesheet,
 			&user.CustomJS,
+			&user.ExternalFontHosts,
 			&user.GoogleID,
 			&user.OpenIDConnectID,
 			&user.DisplayMode,
diff --git a/internal/template/templates/common/layout.html b/internal/template/templates/common/layout.html
index 55a6263f2d7..13c8c652556 100644
--- a/internal/template/templates/common/layout.html
+++ b/internal/template/templates/common/layout.html
@@ -35,16 +35,18 @@
     <link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" .theme "checksum" .theme_checksum }}">
 
     {{ if .user }}
-    {{ $cspNonce := nonce }}
-    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; style-src 'self'{{ if .user.Stylesheet }} 'nonce-{{ $cspNonce }}'{{ end }}{{ if .user.CustomJS }}; script-src 'self' 'nonce-{{ $cspNonce }}'{{ end }}; require-trusted-types-for 'script'; trusted-types ttpolicy;">
-    {{ if .user.Stylesheet }}
-    <style nonce="{{ $cspNonce }}">{{ .user.Stylesheet | safeCSS }}</style>
-    {{ end }}
-    {{ if .user.CustomJS }}
-    <script type="module" nonce="{{ $cspNonce }}">{{ .user.CustomJS | safeJS }}</script>
-    {{ end }}
+        {{ $cspNonce := nonce }}
+        <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; {{ if .user.ExternalFontHosts }}font-src {{ .user.ExternalFontHosts }}; {{ end }}style-src 'self'{{ if .user.Stylesheet }}{{ if .user.ExternalFontHosts }} {{ .user.ExternalFontHosts }}{{ end }} 'nonce-{{ $cspNonce }}'{{ end }}{{ if .user.CustomJS }}; script-src 'self' 'nonce-{{ $cspNonce }}'{{ end }}; require-trusted-types-for 'script'; trusted-types ttpolicy;">
+
+        {{ if .user.Stylesheet }}
+        <style nonce="{{ $cspNonce }}">{{ .user.Stylesheet | safeCSS }}</style>
+        {{ end }}
+
+        {{ if .user.CustomJS }}
+        <script type="module" nonce="{{ $cspNonce }}">{{ .user.CustomJS | safeJS }}</script>
+        {{ end }}
     {{ else }}
-    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; require-trusted-types-for 'script'; trusted-types ttpolicy;">
+        <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; require-trusted-types-for 'script'; trusted-types ttpolicy;">
     {{ end }}
 
     <script src="{{ route "javascript" "name" "app" "checksum" .app_js_checksum }}" defer></script>
diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html
index 535c5a1a53a..c584e02a994 100644
--- a/internal/template/templates/views/settings.html
+++ b/internal/template/templates/views/settings.html
@@ -210,6 +210,10 @@ <h1 id="page-header-title">{{ t "page.settings.title" }}</h1>
         <label for="form-custom-css">{{t "form.prefs.label.custom_css" }}</label>
         <textarea id="form-custom-css" name="custom_css" cols="40" rows="10" spellcheck="false">{{ .form.CustomCSS }}</textarea>
 
+        <label for="form-external-font-hosts">{{t "form.prefs.label.external_font_hosts" }}</label>
+        <input type="text" id="form-external-font-hosts" name="external_font_hosts" spellcheck="false" value="{{ .form.ExternalFontHosts }}">
+        <div class="form-help">{{t "form.prefs.help.external_font_hosts" }}</div>
+
         <label for="form-custom-js">{{t "form.prefs.label.custom_js" }}</label>
         <textarea id="form-custom-js" name="custom_js" cols="40" rows="10" spellcheck="false">{{ .form.CustomJS }}</textarea>
 
diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go
index cf18bd2ec84..1b9e48ddeae 100644
--- a/internal/ui/form/settings.go
+++ b/internal/ui/form/settings.go
@@ -10,6 +10,7 @@ import (
 	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/locale"
 	"miniflux.app/v2/internal/model"
+	"miniflux.app/v2/internal/validator"
 )
 
 // MarkReadBehavior list all possible behaviors for automatically marking an entry as read
@@ -37,6 +38,7 @@ type SettingsForm struct {
 	ShowReadingTime        bool
 	CustomCSS              string
 	CustomJS               string
+	ExternalFontHosts      string
 	EntrySwipe             bool
 	GestureNav             string
 	DisplayMode            string
@@ -101,6 +103,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
 	user.ShowReadingTime = s.ShowReadingTime
 	user.Stylesheet = s.CustomCSS
 	user.CustomJS = s.CustomJS
+	user.ExternalFontHosts = s.ExternalFontHosts
 	user.EntrySwipe = s.EntrySwipe
 	user.GestureNav = s.GestureNav
 	user.DisplayMode = s.DisplayMode
@@ -148,6 +151,12 @@ func (s *SettingsForm) Validate() *locale.LocalizedError {
 		return locale.NewLocalizedError("error.settings_media_playback_rate_range")
 	}
 
+	if s.ExternalFontHosts != "" {
+		if !validator.IsValidDomainList(s.ExternalFontHosts) {
+			return locale.NewLocalizedError("error.settings_invalid_domain_list")
+		}
+	}
+
 	return nil
 }
 
@@ -183,6 +192,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
 		ShowReadingTime:        r.FormValue("show_reading_time") == "1",
 		CustomCSS:              r.FormValue("custom_css"),
 		CustomJS:               r.FormValue("custom_js"),
+		ExternalFontHosts:      r.FormValue("external_font_hosts"),
 		EntrySwipe:             r.FormValue("entry_swipe") == "1",
 		GestureNav:             r.FormValue("gesture_nav"),
 		DisplayMode:            r.FormValue("display_mode"),
diff --git a/internal/ui/settings_show.go b/internal/ui/settings_show.go
index 72e1f5ab20c..179b98025f1 100644
--- a/internal/ui/settings_show.go
+++ b/internal/ui/settings_show.go
@@ -34,6 +34,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
 		ShowReadingTime:        user.ShowReadingTime,
 		CustomCSS:              user.Stylesheet,
 		CustomJS:               user.CustomJS,
+		ExternalFontHosts:      user.ExternalFontHosts,
 		EntrySwipe:             user.EntrySwipe,
 		GestureNav:             user.GestureNav,
 		DisplayMode:            user.DisplayMode,
diff --git a/internal/ui/settings_update.go b/internal/ui/settings_update.go
index 0e03752d5e2..be99adb548f 100644
--- a/internal/ui/settings_update.go
+++ b/internal/ui/settings_update.go
@@ -85,6 +85,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
 		MediaPlaybackRate:     model.OptionalNumber(settingsForm.MediaPlaybackRate),
 		BlockFilterEntryRules: model.OptionalString(settingsForm.BlockFilterEntryRules),
 		KeepFilterEntryRules:  model.OptionalString(settingsForm.KeepFilterEntryRules),
+		ExternalFontHosts:     model.OptionalString(settingsForm.ExternalFontHosts),
 	}
 
 	if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil {
diff --git a/internal/ui/static/css/common.css b/internal/ui/static/css/common.css
index 6ffaa8bf091..2bd3a535986 100644
--- a/internal/ui/static/css/common.css
+++ b/internal/ui/static/css/common.css
@@ -427,7 +427,6 @@ input[type="number"] {
     line-height: 20px;
     width: 250px;
     font-size: 99%;
-    margin-bottom: 10px;
     margin-top: 5px;
     appearance: none;
 }
@@ -448,7 +447,8 @@ input[type="number"]:focus {
 }
 
 input[type="checkbox"] {
-    margin-bottom: 15px;
+    margin-top: 10px;
+    margin-bottom: 10px;
 }
 
 textarea {
diff --git a/internal/validator/user.go b/internal/validator/user.go
index 2365ee61da4..2e79785b407 100644
--- a/internal/validator/user.go
+++ b/internal/validator/user.go
@@ -123,6 +123,12 @@ func ValidateUserModification(store *storage.Storage, userID int64, changes *mod
 		}
 	}
 
+	if changes.ExternalFontHosts != nil {
+		if !IsValidDomainList(*changes.ExternalFontHosts) {
+			return locale.NewLocalizedError("error.settings_invalid_domain_list")
+		}
+	}
+
 	return nil
 }
 
diff --git a/internal/validator/validator.go b/internal/validator/validator.go
index 63fe75f0882..9b3cfd908b3 100644
--- a/internal/validator/validator.go
+++ b/internal/validator/validator.go
@@ -7,8 +7,11 @@ import (
 	"fmt"
 	"net/url"
 	"regexp"
+	"strings"
 )
 
+var domainRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
+
 // ValidateRange makes sure the offset/limit values are valid.
 func ValidateRange(offset, limit int) error {
 	if offset < 0 {
@@ -43,3 +46,24 @@ func IsValidURL(absoluteURL string) bool {
 	_, err := url.ParseRequestURI(absoluteURL)
 	return err == nil
 }
+
+func IsValidDomain(domain string) bool {
+	domain = strings.ToLower(domain)
+
+	if len(domain) < 1 || len(domain) > 253 {
+		return false
+	}
+
+	return domainRegex.MatchString(domain)
+}
+
+func IsValidDomainList(value string) bool {
+	domains := strings.Split(strings.TrimSpace(value), " ")
+	for _, domain := range domains {
+		if !IsValidDomain(domain) {
+			return false
+		}
+	}
+
+	return true
+}


Test patch:
diff --git a/internal/api/api_integration_test.go b/internal/api/api_integration_test.go
index 1a95cf08bf3..fe172ce587d 100644
--- a/internal/api/api_integration_test.go
+++ b/internal/api/api_integration_test.go
@@ -592,6 +592,59 @@ func TestUpdateUserEndpointByChangingDefaultTheme(t *testing.T) {
 	}
 }
 
+func TestUpdateUserEndpointByChangingExternalFonts(t *testing.T) {
+	testConfig := newIntegrationTestConfig()
+	if !testConfig.isConfigured() {
+		t.Skip(skipIntegrationTestsMessage)
+	}
+
+	adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
+	regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer adminClient.DeleteUser(regularTestUser.ID)
+
+	regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
+
+	userUpdateRequest := &miniflux.UserModificationRequest{
+		ExternalFontHosts: miniflux.SetOptionalField("  fonts.example.org  "),
+	}
+
+	updatedUser, err := regularUserClient.UpdateUser(regularTestUser.ID, userUpdateRequest)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if updatedUser.ExternalFontHosts != "fonts.example.org" {
+		t.Fatalf(`Invalid external font hosts, got "%v"`, updatedUser.ExternalFontHosts)
+	}
+}
+
+func TestUpdateUserEndpointByChangingExternalFontsWithInvalidValue(t *testing.T) {
+	testConfig := newIntegrationTestConfig()
+	if !testConfig.isConfigured() {
+		t.Skip(skipIntegrationTestsMessage)
+	}
+
+	adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
+	regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer adminClient.DeleteUser(regularTestUser.ID)
+
+	regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
+
+	userUpdateRequest := &miniflux.UserModificationRequest{
+		ExternalFontHosts: miniflux.SetOptionalField("'self' *"),
+	}
+
+	if _, err := regularUserClient.UpdateUser(regularTestUser.ID, userUpdateRequest); err == nil {
+		t.Fatal(`Updating the user with an invalid external font host should raise an error`)
+	}
+}
+
 func TestUpdateUserEndpointByChangingCustomJS(t *testing.T) {
 	testConfig := newIntegrationTestConfig()
 	if !testConfig.isConfigured() {
diff --git a/internal/validator/validator_test.go b/internal/validator/validator_test.go
index 0a51973be70..7121a111f99 100644
--- a/internal/validator/validator_test.go
+++ b/internal/validator/validator_test.go
@@ -59,3 +59,21 @@ func TestIsValidRegex(t *testing.T) {
 		}
 	}
 }
+
+func TestIsValidDomain(t *testing.T) {
+	scenarios := map[string]bool{
+		"example.org":          true,
+		"example":              false,
+		"example.":             false,
+		"example..":            false,
+		"mail.example.com:443": false,
+		"*.example.com":        false,
+	}
+
+	for domain, expected := range scenarios {
+		result := IsValidDomain(domain)
+		if result != expected {
+			t.Errorf(`Unexpected result, got %v instead of %v`, result, expected)
+		}
+	}
+}