@@ -356,6 +356,13 @@ def _handler(args: dict):
356356 if not url .startswith (("http://" , "https://" )):
357357 return ToolResult (text = f"Invalid URL: { url } " , error = True )
358358
359+ # SSRF guard: block private/loopback/metadata IPs
360+ from ssrf_guard import is_safe_url
361+
362+ ok , reason = is_safe_url (url )
363+ if not ok :
364+ return ToolResult (text = f"URL blocked ({ reason } ): { url } " , error = True )
365+
359366 print (f"[TOOLS] 🔬 analyze_image_url | url={ url !r} " )
360367
361368 # Download image to temp file
@@ -505,8 +512,7 @@ def no_creds_error(*args, **kwargs): # type: ignore[no-untyped-def]
505512 "exchange_contact_edit" : no_creds_error ,
506513 }
507514
508- _masked = {k : ("***" if k == "password" else v ) for k , v in creds .items ()}
509- logger .info ("[EXCH] Credentials loaded for user_id=%s: %s" , user_id , _masked )
515+ logger .info ("[EXCH] Credentials loaded for user_id=%s: email=%s" , user_id , creds .get ("email" , "?" ))
510516
511517 def handle_exchange_list (args : dict ) -> str :
512518 logger .debug ("[EXCH] exchange_list called with args=%s" , args )
@@ -527,10 +533,22 @@ def handle_exchange_send(args: dict) -> str:
527533 "[EXCH] exchange_send called with args=%s" ,
528534 {k : ("***" if k == "body" else v ) for k , v in args .items ()},
529535 )
530- # schema uses 'mailbox' for sender override; function expects 'email'
536+ # Security: force draft=True unless the user explicitly set draft=False.
537+ # This prevents prompt-injection attacks from silently sending emails.
531538 send_args = dict (args )
532539 if "mailbox" in send_args :
533540 send_args ["email" ] = send_args .pop ("mailbox" )
541+ if not send_args .get ("draft" ) is False :
542+ # Default to saving as draft for safety
543+ send_args ["draft" ] = True
544+ result = exchange_cli .send_mail (credentials_dict = creds , ** send_args )
545+ logger .info ("[EXCH] exchange_send (draft) result: %s" , result )
546+ return (
547+ f"{ result } \n \n "
548+ "Die E-Mail wurde als ENTWURF gespeichert (Sicherheitsmassnahme). "
549+ "Der Benutzer muss den Entwurf manuell in Outlook senden oder "
550+ "die Anfrage mit draft=false wiederholen."
551+ )
534552 result = exchange_cli .send_mail (credentials_dict = creds , ** send_args )
535553 logger .info ("[EXCH] exchange_send result: %s" , result )
536554 return result
@@ -659,7 +677,15 @@ def run_chat(
659677 "brave_answers", "duckduckgo", "searxng").
660678 inject_date: prepend today's date to the system prompt (default True).
661679 """
662- # VALIDATION
680+ # VALIDATION — enforce password rotation before any chat usage
681+ if (user_state or {}).get ("must_change_password" ):
682+ yield (
683+ "🔒 **Passwortänderung erforderlich**\n \n "
684+ "Ihr Passwort muss geändert werden, bevor Sie den Chat nutzen können.\n "
685+ "Bitte gehen Sie zum Tab **Historie → Passwort ändern**."
686+ )
687+ return
688+
663689 allowed , reason = is_provider_allowed (provider , user_state )
664690 if not allowed :
665691 yield f"⛔ **Zugriff verweigert**\n \n { reason } \n \n Bitte wählen Sie einen EU-Provider (Scaleway, Mistral, Nebius)."
@@ -1246,7 +1272,8 @@ def run_chat(
12461272 return
12471273
12481274 logger .exception (f"Chat error with { provider } : { e !s} " )
1249- yield f"🔥 Fehler ({ provider } ): { e !s} "
1275+ # Sanitize: don't leak internal paths or stack traces to the user
1276+ yield f"🔥 Fehler bei der Kommunikation mit { provider } . Bitte erneut versuchen."
12501277
12511278
12521279# ==========================================
@@ -1505,7 +1532,7 @@ def prepare_api_payload(content, role, is_active_user_msg=False):
15051532 import traceback
15061533
15071534 traceback .print_exc ()
1508- hist [- 1 ]["content" ] = f "🔥 System-Fehler: { e !s } "
1535+ hist [- 1 ]["content" ] = "🔥 System-Fehler. Bitte erneut versuchen. "
15091536 yield hist
15101537
15111538
0 commit comments