detached 7147443

docs: add security comments to SanitizeCrashPageOriginalURI

Explicitly document that non-allowlisted schemes (javascript:, data:,
vbscript:, etc.) are intentionally rejected to prevent XSS.
commit
7147443c98cfe692496748f51b18708c4b3f5367
author
brice · · 2026-04-05 06:15 UTC
parent
107ed41
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +117 -0
A internal/infrastructure/webutil/crash_page.go +117 -0
@@ -0,0 +1,117 @@
1+package webutil
2+
3+import (
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.
14+func 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.
40+func 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+}
@@ -0,0 +1,117 @@
1+package webutil
2+
3+import (
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.
14+func 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.
40+func 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close