Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
22 lines
1.3 KiB
Python
22 lines
1.3 KiB
Python
from django.contrib.auth.views import LogoutView
|
|
from django.urls import path
|
|
from . import views
|
|
|
|
urlpatterns = [
|
|
path('register/', views.register, name='register'),
|
|
path('login/', views.login_view, name='login'),
|
|
path('logout/', LogoutView.as_view(), name='logout'),
|
|
path('settings/', views.settings_view, name='settings'),
|
|
path('lastfm/connect/', views.lastfm_connect, name='lastfm_connect'),
|
|
path('lastfm/callback/', views.lastfm_callback, name='lastfm_callback'),
|
|
path('lastfm/disconnect/', views.lastfm_disconnect, name='lastfm_disconnect'),
|
|
path('background/upload/', views.upload_background, name='upload_background'),
|
|
path('background/delete/', views.delete_background, name='delete_background'),
|
|
path('focus-station/', views.save_focus_station, name='save_focus_station'),
|
|
path('check-password/', views.check_password, name='check_password'),
|
|
path('change-password/', views.change_password, name='change_password'),
|
|
path('api-token/regenerate/', views.regenerate_api_token, name='regenerate_api_token'),
|
|
path('webdav/add/', views.webdav_add, name='webdav_add'),
|
|
path('webdav/<int:pk>/test/', views.webdav_test, name='webdav_test'),
|
|
path('webdav/<int:pk>/delete/', views.webdav_delete, name='webdav_delete'),
|
|
]
|