55import time
66from datetime import datetime , timedelta , timezone
77from google .cloud import datastore
8+ from utils .safe_encoding import escape_html , build_safe_url
89
910logger = logging .getLogger (__name__ )
1011
@@ -259,14 +260,14 @@ def generate_mobile_exposure_cards(user_exposures: list) -> str:
259260 cards += f"""
260261 <div class="mobile-card">
261262 <div style="font-weight: bold; color: #e74c3c; font-size: 16px; margin-bottom: 8px;">
262- 🚨 { exposure .get ('breach_name' , 'Unknown' )}
263+ 🚨 { escape_html ( exposure .get ('breach_name' , 'Unknown' ) )}
263264 </div>
264265 <div style="color: #495057; font-size: 14px; line-height: 1.4;">
265- <div style="margin: 4px 0;"><strong>Breached:</strong> { exposure .get ('breach_date' , 'Unknown' )} </div>
266- <div style="margin: 4px 0;"><strong>Added:</strong> { exposure .get ('added_date' , 'Unknown' )} </div>
266+ <div style="margin: 4px 0;"><strong>Breached:</strong> { escape_html ( exposure .get ('breach_date' , 'Unknown' ) )} </div>
267+ <div style="margin: 4px 0;"><strong>Added:</strong> { escape_html ( exposure .get ('added_date' , 'Unknown' ) )} </div>
267268 <div style="margin: 4px 0;"><strong>Records:</strong> { exposure .get ('records_count' , 0 ):,} </div>
268269 <div style="margin: 8px 0 0 0; padding: 8px; background-color: #f8f9fa; border-radius: 4px; font-size: 12px;">
269- <strong>Data Exposed:</strong><br>{ exposure .get ('data_exposed' , 'Unknown' )}
270+ <strong>Data Exposed:</strong><br>{ escape_html ( exposure .get ('data_exposed' , 'Unknown' ) )}
270271 </div>
271272 </div>
272273 </div>"""
@@ -286,14 +287,14 @@ def generate_mobile_breach_cards(new_breaches: list) -> str:
286287 cards += f"""
287288 <div class="mobile-card">
288289 <div style="font-weight: bold; color: #f39c12; font-size: 16px; margin-bottom: 8px;">
289- 🚨 { breach .get ('breach_name' , 'Unknown' )}
290+ 🚨 { escape_html ( breach .get ('breach_name' , 'Unknown' ) )}
290291 </div>
291292 <div style="color: #495057; font-size: 14px; line-height: 1.4;">
292- <div style="margin: 4px 0;"><strong>Breached:</strong> { breach .get ('breach_date' , 'Unknown' )} </div>
293- <div style="margin: 4px 0;"><strong>Added:</strong> { breach .get ('added_date' , 'Unknown' )} </div>
293+ <div style="margin: 4px 0;"><strong>Breached:</strong> { escape_html ( breach .get ('breach_date' , 'Unknown' ) )} </div>
294+ <div style="margin: 4px 0;"><strong>Added:</strong> { escape_html ( breach .get ('added_date' , 'Unknown' ) )} </div>
294295 <div style="margin: 4px 0;"><strong>Records:</strong> { breach .get ('records_count' , 0 ):,} </div>
295296 <div style="margin: 8px 0 0 0; padding: 8px; background-color: #f8f9fa; border-radius: 4px; font-size: 12px;">
296- <strong>Data Exposed:</strong><br>{ breach .get ('data_exposed' , 'Unknown' )}
297+ <strong>Data Exposed:</strong><br>{ escape_html ( breach .get ('data_exposed' , 'Unknown' ) )}
297298 </div>
298299 </div>
299300 </div>"""
@@ -318,15 +319,22 @@ async def generate_html_template(
318319 # Use the verified domains passed to the function (not derived from exposures)
319320 # user_domains parameter already contains the verified domains for this user
320321
321- # Summary info
322- domains_text = ", " .join (user_domains ) if user_domains else "No verified domains"
322+ # Summary info (escape domains for safe HTML display)
323+ domains_text = (
324+ ", " .join (escape_html (d ) for d in user_domains )
325+ if user_domains
326+ else "No verified domains"
327+ )
323328 summary_info = (
324329 "Your summary: <strong>{} verified domains</strong> ({}) • "
325330 "<strong>{} exposures</strong> • <strong>{} new breaches</strong> this month"
326331 ).format (len (user_domains ), domains_text , len (user_exposures ), len (new_breaches ))
327332
328- # Dashboard URL
329- dashboard_url = f"https://xposedornot.com/breach-dashboard?email={ email } &token={ dashboard_token } "
333+ # Dashboard URL with properly encoded parameters
334+ dashboard_url = build_safe_url (
335+ "https://xposedornot.com/breach-dashboard" ,
336+ {"email" : email , "token" : dashboard_token },
337+ )
330338
331339 # Build mobile-friendly cards instead of table rows
332340 exposure_cards = ""
@@ -335,14 +343,14 @@ async def generate_html_template(
335343 exposure_cards += f"""
336344 <div style="border: 1px solid #dee2e6; border-radius: 6px; padding: 12px; margin: 8px 0; background-color: #fff;">
337345 <div style="font-weight: bold; color: #e74c3c; font-size: 16px; margin-bottom: 8px;">
338- 🚨 { exposure .get ('breach_name' , 'Unknown' )}
346+ 🚨 { escape_html ( exposure .get ('breach_name' , 'Unknown' ) )}
339347 </div>
340348 <div style="color: #495057; font-size: 14px; line-height: 1.4;">
341- <div style="margin: 4px 0;"><strong>Breached:</strong> { exposure .get ('breach_date' , 'Unknown' )} </div>
342- <div style="margin: 4px 0;"><strong>Added:</strong> { exposure .get ('added_date' , 'Unknown' )} </div>
349+ <div style="margin: 4px 0;"><strong>Breached:</strong> { escape_html ( exposure .get ('breach_date' , 'Unknown' ) )} </div>
350+ <div style="margin: 4px 0;"><strong>Added:</strong> { escape_html ( exposure .get ('added_date' , 'Unknown' ) )} </div>
343351 <div style="margin: 4px 0;"><strong>Records:</strong> { exposure .get ('records_count' , 0 ):,} </div>
344352 <div style="margin: 8px 0 0 0; padding: 8px; background-color: #f8f9fa; border-radius: 4px; font-size: 12px;">
345- <strong>Data Exposed:</strong><br>{ exposure .get ('data_exposed' , 'Unknown' )}
353+ <strong>Data Exposed:</strong><br>{ escape_html ( exposure .get ('data_exposed' , 'Unknown' ) )}
346354 </div>
347355 </div>
348356 </div>"""
@@ -360,14 +368,14 @@ async def generate_html_template(
360368 breach_cards += f"""
361369 <div style="border: 1px solid #dee2e6; border-radius: 6px; padding: 12px; margin: 8px 0; background-color: #fff;">
362370 <div style="font-weight: bold; color: #f39c12; font-size: 16px; margin-bottom: 8px;">
363- 🚨 { breach .get ('breach_name' , 'Unknown' )}
371+ 🚨 { escape_html ( breach .get ('breach_name' , 'Unknown' ) )}
364372 </div>
365373 <div style="color: #495057; font-size: 14px; line-height: 1.4;">
366- <div style="margin: 4px 0;"><strong>Breached:</strong> { breach .get ('breach_date' , 'Unknown' )} </div>
367- <div style="margin: 4px 0;"><strong>Added:</strong> { breach .get ('added_date' , 'Unknown' )} </div>
374+ <div style="margin: 4px 0;"><strong>Breached:</strong> { escape_html ( breach .get ('breach_date' , 'Unknown' ) )} </div>
375+ <div style="margin: 4px 0;"><strong>Added:</strong> { escape_html ( breach .get ('added_date' , 'Unknown' ) )} </div>
368376 <div style="margin: 4px 0;"><strong>Records:</strong> { breach .get ('records_count' , 0 ):,} </div>
369377 <div style="margin: 8px 0 0 0; padding: 8px; background-color: #f8f9fa; border-radius: 4px; font-size: 12px;">
370- <strong>Data Exposed:</strong><br>{ breach .get ('data_exposed' , 'Unknown' )}
378+ <strong>Data Exposed:</strong><br>{ escape_html ( breach .get ('data_exposed' , 'Unknown' ) )}
371379 </div>
372380 </div>
373381 </div>"""
@@ -448,7 +456,7 @@ async def generate_html_template(
448456
449457 <!-- Email Footer -->
450458 <div style="background-color: #f8f9fa; padding: 20px; margin-top: 30px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d; text-align: center;">
451- <p style="margin: 0 0 10px 0;">This email was sent to <strong>{ email } </strong> because of monthly breach notifications in XposedOrNot.com.</p>
459+ <p style="margin: 0 0 10px 0;">This email was sent to <strong>{ escape_html ( email ) } </strong> because of monthly breach notifications in XposedOrNot.com.</p>
452460 <p style="margin: 0;">© 2025 XposedOrNot. All rights reserved. | <a href="https://xposedornot.com/dashboard" style="color: #6c757d;">Visit Website</a></p>
453461 </div>
454462
0 commit comments