Skip to content

Commit 6464002

Browse files
security: fix XSS vulnerability with embedded HTML templates
Move OAuth callback HTML pages to embedded template files and use html/template which auto-escapes all template variables by default. - Add internal/oauth/templates/error.html for authorization failures - Add internal/oauth/templates/success.html for successful auth - Use embed.FS to embed templates at compile time - html/template.Execute auto-escapes ErrorMessage, fixing XSS The error message from OAuth providers is now safely escaped before being rendered in the HTML response, preventing reflected XSS attacks.
1 parent 30a62d4 commit 6464002

File tree

3 files changed

+156
-127
lines changed

3 files changed

+156
-127
lines changed

internal/oauth/oauth.go

Lines changed: 29 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package oauth
22

33
import (
44
"crypto/rand"
5+
"embed"
56
"encoding/base64"
67
"fmt"
8+
"html/template"
79
"io"
810
"net"
911
"net/http"
@@ -14,6 +16,26 @@ import (
1416
"time"
1517
)
1618

19+
//go:embed templates/*.html
20+
var templateFS embed.FS
21+
22+
var (
23+
errorTemplate *template.Template
24+
successTemplate *template.Template
25+
)
26+
27+
func init() {
28+
var err error
29+
errorTemplate, err = template.ParseFS(templateFS, "templates/error.html")
30+
if err != nil {
31+
panic(fmt.Sprintf("failed to parse error template: %v", err))
32+
}
33+
successTemplate, err = template.ParseFS(templateFS, "templates/success.html")
34+
if err != nil {
35+
panic(fmt.Sprintf("failed to parse success template: %v", err))
36+
}
37+
}
38+
1739
const (
1840
// DefaultAuthTimeout is the default timeout for the OAuth authorization flow
1941
DefaultAuthTimeout = 5 * time.Minute
@@ -95,70 +117,10 @@ func createCallbackHandler(expectedState string, codeChan chan<- string, errChan
95117
errChan <- fmt.Errorf("authorization failed: %s", errMsg)
96118

97119
w.Header().Set("Content-Type", "text/html; charset=utf-8")
98-
fmt.Fprintf(w, `<!DOCTYPE html>
99-
<html>
100-
<head>
101-
<meta charset="utf-8">
102-
<title>Authorization Failed</title>
103-
<style>
104-
body {
105-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
106-
background: linear-gradient(135deg, #0d1117 0%%, #161b22 100%%);
107-
color: #e6edf3;
108-
min-height: 100vh;
109-
margin: 0;
110-
display: flex;
111-
align-items: center;
112-
justify-content: center;
113-
}
114-
.container {
115-
text-align: center;
116-
padding: 48px;
117-
background: #21262d;
118-
border-radius: 12px;
119-
border: 1px solid #30363d;
120-
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
121-
max-width: 400px;
122-
}
123-
.icon {
124-
width: 64px;
125-
height: 64px;
126-
margin-bottom: 24px;
127-
}
128-
h1 {
129-
color: #f85149;
130-
font-size: 24px;
131-
font-weight: 600;
132-
margin: 0 0 16px 0;
133-
}
134-
p {
135-
color: #8b949e;
136-
margin: 8px 0;
137-
line-height: 1.5;
138-
}
139-
.error {
140-
background: #21262d;
141-
border: 1px solid #f8514966;
142-
border-radius: 6px;
143-
padding: 12px;
144-
margin-top: 16px;
145-
font-family: monospace;
146-
font-size: 13px;
147-
color: #f85149;
148-
}
149-
</style>
150-
</head>
151-
<body>
152-
<div class="container">
153-
<svg class="icon" viewBox="0 0 24 24" fill="#e6edf3">
154-
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
155-
</svg>
156-
<h1>Authorization Failed</h1>
157-
<p class="error">%s</p>
158-
<p>You can close this window.</p>
159-
</div>
160-
</body>
161-
</html>`, errMsg)
120+
// html/template auto-escapes ErrorMessage to prevent XSS
121+
if err := errorTemplate.Execute(w, struct{ ErrorMessage string }{ErrorMessage: errMsg}); err != nil {
122+
http.Error(w, "Internal error", http.StatusInternalServerError)
123+
}
162124
return
163125
}
164126

@@ -182,69 +144,9 @@ p {
182144

183145
// Display success page
184146
w.Header().Set("Content-Type", "text/html; charset=utf-8")
185-
fmt.Fprint(w, `<!DOCTYPE html>
186-
<html>
187-
<head>
188-
<meta charset="utf-8">
189-
<title>Authorization Successful</title>
190-
<style>
191-
body {
192-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
193-
background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
194-
color: #e6edf3;
195-
min-height: 100vh;
196-
margin: 0;
197-
display: flex;
198-
align-items: center;
199-
justify-content: center;
200-
}
201-
.container {
202-
text-align: center;
203-
padding: 48px;
204-
background: #21262d;
205-
border-radius: 12px;
206-
border: 1px solid #30363d;
207-
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
208-
max-width: 400px;
209-
}
210-
.icon {
211-
width: 64px;
212-
height: 64px;
213-
margin-bottom: 24px;
214-
}
215-
h1 {
216-
color: #3fb950;
217-
font-size: 24px;
218-
font-weight: 600;
219-
margin: 0 0 16px 0;
220-
}
221-
p {
222-
color: #8b949e;
223-
margin: 8px 0;
224-
line-height: 1.5;
225-
}
226-
.hint {
227-
margin-top: 24px;
228-
padding: 12px;
229-
background: #161b22;
230-
border-radius: 6px;
231-
font-size: 14px;
232-
}
233-
</style>
234-
</head>
235-
<body>
236-
<div class="container">
237-
<svg class="icon" viewBox="0 0 24 24" fill="#e6edf3">
238-
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
239-
</svg>
240-
<h1>Authorization Successful</h1>
241-
<p>You have successfully authorized the GitHub MCP Server.</p>
242-
<div class="hint">
243-
<p>You can close this window and retry your request.</p>
244-
</div>
245-
</div>
246-
</body>
247-
</html>`)
147+
if err := successTemplate.Execute(w, nil); err != nil {
148+
http.Error(w, "Internal error", http.StatusInternalServerError)
149+
}
248150
})
249151

250152
return mux
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Authorization Failed</title>
6+
<style>
7+
body {
8+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
9+
background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
10+
color: #e6edf3;
11+
min-height: 100vh;
12+
margin: 0;
13+
display: flex;
14+
align-items: center;
15+
justify-content: center;
16+
}
17+
.container {
18+
text-align: center;
19+
padding: 48px;
20+
background: #21262d;
21+
border-radius: 12px;
22+
border: 1px solid #30363d;
23+
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
24+
max-width: 400px;
25+
}
26+
.icon {
27+
width: 64px;
28+
height: 64px;
29+
margin-bottom: 24px;
30+
}
31+
h1 {
32+
color: #f85149;
33+
font-size: 24px;
34+
font-weight: 600;
35+
margin: 0 0 16px 0;
36+
}
37+
p {
38+
color: #8b949e;
39+
margin: 8px 0;
40+
line-height: 1.5;
41+
}
42+
.error {
43+
background: #21262d;
44+
border: 1px solid #f8514966;
45+
border-radius: 6px;
46+
padding: 12px;
47+
margin-top: 16px;
48+
font-family: monospace;
49+
font-size: 13px;
50+
color: #f85149;
51+
}
52+
</style>
53+
</head>
54+
<body>
55+
<div class="container">
56+
<svg class="icon" viewBox="0 0 24 24" fill="#e6edf3">
57+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
58+
</svg>
59+
<h1>Authorization Failed</h1>
60+
<p class="error">{{.ErrorMessage}}</p>
61+
<p>You can close this window.</p>
62+
</div>
63+
</body>
64+
</html>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Authorization Successful</title>
6+
<style>
7+
body {
8+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
9+
background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
10+
color: #e6edf3;
11+
min-height: 100vh;
12+
margin: 0;
13+
display: flex;
14+
align-items: center;
15+
justify-content: center;
16+
}
17+
.container {
18+
text-align: center;
19+
padding: 48px;
20+
background: #21262d;
21+
border-radius: 12px;
22+
border: 1px solid #30363d;
23+
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
24+
max-width: 400px;
25+
}
26+
.icon {
27+
width: 64px;
28+
height: 64px;
29+
margin-bottom: 24px;
30+
}
31+
h1 {
32+
color: #3fb950;
33+
font-size: 24px;
34+
font-weight: 600;
35+
margin: 0 0 16px 0;
36+
}
37+
p {
38+
color: #8b949e;
39+
margin: 8px 0;
40+
line-height: 1.5;
41+
}
42+
.hint {
43+
margin-top: 24px;
44+
padding: 12px;
45+
background: #161b22;
46+
border-radius: 6px;
47+
font-size: 14px;
48+
}
49+
</style>
50+
</head>
51+
<body>
52+
<div class="container">
53+
<svg class="icon" viewBox="0 0 24 24" fill="#e6edf3">
54+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
55+
</svg>
56+
<h1>Authorization Successful</h1>
57+
<p>You have successfully authorized the GitHub MCP Server.</p>
58+
<div class="hint">
59+
<p>You can close this window and retry your request.</p>
60+
</div>
61+
</div>
62+
</body>
63+
</html>

0 commit comments

Comments
 (0)