1package webutil
2
3import (
4 "fmt"
5 "html"
6 "net/url"
7 "strings"
8)
9
10// SanitizeCrashPageOriginalURI validates and sanitizes a URI for display on a crash page.
11// Only http, https, and dumb schemes are allowed. Non-allowlisted schemes such as
12// javascript:, data:, and vbscript: are intentionally rejected to prevent XSS when
13// the URI is rendered into crash page HTML.
14func SanitizeCrashPageOriginalURI(originalURI string) string {
15 originalURI = strings.TrimSpace(originalURI)
16 if originalURI == "" {
17 return ""
18 }
19 parsed, err := url.Parse(originalURI)
20 if err != nil {
21 return ""
22 }
23 switch strings.ToLower(parsed.Scheme) {
24 case "http", "https":
25 if parsed.Host == "" {
26 return ""
27 }
28 return parsed.String()
29 case "dumb":
30 if parsed.Host == "" && parsed.Opaque == "" {
31 return ""
32 }
33 return parsed.String()
34 default: // Security: reject all non-allowlisted schemes (javascript:, data:, vbscript:, etc.)
35 return ""
36 }
37}
38
39// BuildCrashPageHTML returns a self-contained HTML page for renderer crash recovery.
40func BuildCrashPageHTML(originalURI string) string {
41 escapedURI := html.EscapeString(originalURI)
42 return fmt.Sprintf(`<!DOCTYPE html>
43<html lang="en">
44<head>
45 <meta charset="utf-8">
46 <meta name="viewport" content="width=device-width, initial-scale=1">
47 <title>Renderer crashed</title>
48 <style>
49 :root { color-scheme: dark; font-family: "IBM Plex Sans", "Segoe UI", sans-serif; }
50 body {
51 margin: 0;
52 min-height: 100vh;
53 display: flex;
54 align-items: center;
55 justify-content: center;
56 background: radial-gradient(circle at top, #253447, #101622 55%%);
57 color: #f2f6fa;
58 padding: 24px;
59 }
60 .card {
61 width: min(640px, 100%%);
62 background: rgba(10, 16, 26, 0.86);
63 border: 1px solid rgba(144, 173, 205, 0.35);
64 border-radius: 16px;
65 box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
66 padding: 28px;
67 }
68 .url {
69 margin: 16px 0 20px;
70 padding: 12px;
71 border-radius: 10px;
72 background: rgba(26, 38, 56, 0.85);
73 border: 1px solid rgba(139, 167, 194, 0.28);
74 font-family: "IBM Plex Mono", "Fira Code", monospace;
75 overflow-wrap: anywhere;
76 }
77 .actions { display: flex; gap: 12px; flex-wrap: wrap; }
78 button {
79 border: 0;
80 border-radius: 10px;
81 padding: 10px 16px;
82 cursor: pointer;
83 font-size: 0.95rem;
84 font-weight: 600;
85 }
86 .primary { background: #4dd0e1; color: #061018; }
87 .secondary { background: #233346; color: #d6e5f5; }
88 </style>
89</head>
90<body>
91 <div class="card">
92 <h1>Renderer process ended</h1>
93 <p>The current page was interrupted. You can reload it to continue browsing.</p>
94 <div class="url">%s</div>
95 <div class="actions">
96 <button class="primary" id="reload-btn" data-target="%s">Reload page</button>
97 <button class="secondary" id="stay-btn">Stay on this page</button>
98 </div>
99 </div>
100 <script>
101 const reloadButton = document.getElementById('reload-btn');
102 const targetUrl = (reloadButton.getAttribute('data-target') || '').trim();
103 reloadButton.addEventListener('click', function() {
104 if (targetUrl) {
105 window.location.href = targetUrl;
106 return;
107 }
108 window.location.reload();
109 });
110 document.getElementById('stay-btn').addEventListener('click', function() {
111 this.disabled = true;
112 this.textContent = 'Staying on page';
113 });
114 </script>
115</body>
116</html>`, escapedURI, escapedURI)
117}