From 51d8c2f14eca017b1bda17a6bf7af33326e58ffb Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 1 Jul 2026 13:41:32 +0200 Subject: [PATCH] fix(calendar): SSRF exception for Exchange + error messages + domain in edit - SSRF check skipped for Exchange type (internal EWS servers are common) - testConnectionFromConfig catches SSRF/validation errors, returns {success:false,error} instead of throwing 403 - updateSource reads existing.type to determine effective type for SSRF check - Panel shows saveError/editSaveError on failed add/update - Edit form initialValues now includes domain field - i18n: calendar.saveError key added (de+en) Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/calendar/calendar.service.ts | 18 +++++++++++++----- .../settings/calendar-settings-panel.tsx | 17 ++++++++++++++--- apps/web/src/messages/de.json | 1 + apps/web/src/messages/en.json | 1 + 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/apps/api/src/calendar/calendar.service.ts b/apps/api/src/calendar/calendar.service.ts index 41536ca..4e38a3c 100644 --- a/apps/api/src/calendar/calendar.service.ts +++ b/apps/api/src/calendar/calendar.service.ts @@ -141,7 +141,8 @@ export class CalendarService { * T-05-11: Validates URL against private IP ranges (SSRF). */ async addSource(userId: string, tenantId: string, dto: CreateCalendarSourceDto) { - await this.validateUrlNotPrivate(dto.url); + // Exchange EWS servers are commonly on internal networks — SSRF check skipped for exchange type + if (dto.type !== 'exchange') await this.validateUrlNotPrivate(dto.url); const data: Record = { userId, @@ -174,7 +175,7 @@ export class CalendarService { async updateSource(id: string, userId: string, dto: UpdateCalendarSourceDto) { const existing = await this.prisma.calendarSource.findUnique({ where: { id }, - select: { userId: true }, + select: { userId: true, type: true }, }); if (!existing) { @@ -184,7 +185,9 @@ export class CalendarService { throw new ForbiddenException('Not your calendar source'); } - if (dto.url) { + const effectiveType = dto.type ?? existing.type; + // Exchange EWS servers are commonly on internal networks — SSRF check skipped for exchange type + if (dto.url && effectiveType !== 'exchange') { await this.validateUrlNotPrivate(dto.url); } @@ -287,7 +290,12 @@ export class CalendarService { async testConnectionFromConfig( dto: TestCalendarSourceConfigDto, ): Promise<{ success: boolean; error?: string }> { - await this.validateUrlNotPrivate(dto.url); + try { + // Exchange EWS servers are commonly on internal networks — SSRF check skipped for exchange type + if (dto.type !== 'exchange') await this.validateUrlNotPrivate(dto.url); + } catch (e: any) { + return { success: false, error: e?.message ?? 'URL not allowed' }; + } const provider = this.getProvider(dto.type); const tempSource = { @@ -302,7 +310,7 @@ export class CalendarService { try { const success = await provider.testConnection(tempSource); return { success }; - } catch { + } catch (e: any) { return { success: false, error: 'Connection failed' }; } } diff --git a/apps/web/src/components/settings/calendar-settings-panel.tsx b/apps/web/src/components/settings/calendar-settings-panel.tsx index 88e7f5a..caf6e41 100644 --- a/apps/web/src/components/settings/calendar-settings-panel.tsx +++ b/apps/web/src/components/settings/calendar-settings-panel.tsx @@ -37,6 +37,8 @@ export function CalendarSettingsPanel() { const [editingId, setEditingId] = useState(null); const [deletingId, setDeletingId] = useState(null); const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + const [editSaveError, setEditSaveError] = useState(null); const [testResults, setTestResults] = useState>({}); // Load sources on mount @@ -76,6 +78,7 @@ export function CalendarSettingsPanel() { // Add new source const handleAddSource = useCallback(async (payload: CreateSourcePayload) => { setIsSaving(true); + setSaveError(null); try { const created = await addSource(payload); setSources((prev) => [...prev, created]); @@ -93,11 +96,11 @@ export function CalendarSettingsPanel() { setTestResults((prev) => ({ ...prev, [created.id]: 'error' })); } } catch { - // Error handled silently + setSaveError(t('calendar.saveError') || 'Fehler beim Speichern'); } finally { setIsSaving(false); } - }, []); + }, [t]); // Delete source const handleDeleteSource = useCallback(async (id: string) => { @@ -282,6 +285,9 @@ export function CalendarSettingsPanel() { {/* Edit form (below the source being edited) */} {editingId && (
+ {editSaveError && ( +

{editSaveError}

+ )}

Edit Source

@@ -296,12 +302,14 @@ export function CalendarSettingsPanel() { url: s.url, username: s.username ?? '', exchangeMode: s.exchangeMode ?? undefined, + domain: s.domain ?? undefined, color: s.color ?? undefined, } : undefined; })()} onSave={async (payload) => { setIsSaving(true); + setEditSaveError(null); try { const updated = await updateSource(editingId, payload); setSources((prev) => @@ -309,7 +317,7 @@ export function CalendarSettingsPanel() { ); setEditingId(null); } catch { - // Error handled silently + setEditSaveError(t('calendar.saveError') || 'Fehler beim Speichern'); } finally { setIsSaving(false); } @@ -337,6 +345,9 @@ export function CalendarSettingsPanel() {

{t('calendar.addSource')}

+ {saveError && ( +

{saveError}

+ )} setShowAddForm(false)} diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index f134d46..0f634e4 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -208,6 +208,7 @@ "formTesting": "Teste...", "formTestSuccess": "Verbindung erfolgreich", "formTestFailed": "Verbindung fehlgeschlagen", + "saveError": "Speichern fehlgeschlagen. Bitte Eingaben prüfen.", "formSave": "Speichern", "formSaving": "Wird gespeichert...", "formCancel": "Abbrechen", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 16dc900..8acb6ed 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -208,6 +208,7 @@ "formTesting": "Testing...", "formTestSuccess": "Connection successful", "formTestFailed": "Connection failed", + "saveError": "Save failed. Please check your input.", "formSave": "Save", "formSaving": "Saving...", "formCancel": "Cancel",