Skip to content

Commit 02774f3

Browse files
committed
security checkpoint
1 parent 5d764dc commit 02774f3

File tree

12 files changed

+679
-118
lines changed

12 files changed

+679
-118
lines changed

frontend/src/bundle.js

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
this.container = container;
8484
this.config = config;
8585
this.iframe = null;
86-
this.csrfToken = this.generateCsrfToken();
86+
this.csrfToken = null; // Will be set by server in init()
8787
this.state = {
8888
loading: true,
8989
error: null,
@@ -141,6 +141,9 @@
141141
// Store the signed origin token - will be passed to iframe and used in API calls
142142
this.originToken = initData.token;
143143

144+
// Store the CSRF token from server (cryptographically signed, origin-specific)
145+
this.csrfToken = initData.csrfToken || this.generateCsrfToken(); // Fallback for older servers
146+
144147
} catch (e) {
145148
console.error('[CommentKit] Failed to initialize:', e);
146149
this.state.error = 'Failed to connect to CommentKit. Please try again later.';
@@ -1601,7 +1604,7 @@
16011604
</div>
16021605
<div class="ck-form-group">
16031606
<label for="ck-email">Email (optional)</label>
1604-
<input type="email" id="ck-email" name="email" placeholder="your@email.com">
1607+
<input type="email" id="ck-email" name="email" placeholder="your@email.com" pattern="[^\\s@]+@[^\\s@]+\\.[^\\s@]+" title="Please enter a valid email address (e.g., user@example.com)">
16051608
</div>
16061609
</div>
16071610
<div class="ck-form-group">
@@ -1620,7 +1623,7 @@
16201623
<form id="ck-login-form">
16211624
<div class="ck-form-group">
16221625
<label for="ck-login-email">Email address</label>
1623-
<input type="email" id="ck-login-email" name="email" required placeholder="your@email.com">
1626+
<input type="email" id="ck-login-email" name="email" required placeholder="your@email.com" pattern="[^\\s@]+@[^\\s@]+\\.[^\\s@]+" title="Please enter a valid email address (e.g., user@example.com)">
16241627
</div>
16251628
<p style="font-size: 0.85rem; color: #6b7280; margin-bottom: 12px;">
16261629
We'll send you a magic link to sign in. No password needed.
@@ -1729,7 +1732,7 @@
17291732
</div>
17301733
<div class="ck-form-group">
17311734
<label>Email (optional)</label>
1732-
<input type="email" name="email" placeholder="your@email.com">
1735+
<input type="email" name="email" placeholder="your@email.com" pattern="[^\\s@]+@[^\\s@]+\\.[^\\s@]+" title="Please enter a valid email address (e.g., user@example.com)">
17331736
</div>
17341737
</div>
17351738
<div class="ck-form-group">
@@ -1752,15 +1755,24 @@
17521755
if (form) {
17531756
form.addEventListener('submit', (e) => {
17541757
e.preventDefault();
1758+
1759+
// Use native HTML5 form validation
1760+
if (!form.checkValidity()) {
1761+
form.reportValidity();
1762+
return;
1763+
}
1764+
17551765
const formData = new FormData(form);
1766+
const email = formData.get('email');
1767+
17561768
const commentData = {
17571769
content: formData.get('content'),
17581770
parent_id: null, // Top-level comments have no parent
17591771
};
17601772
// Include guest fields only if not authenticated
17611773
if (!this.state.user) {
17621774
commentData.author_name = formData.get('name');
1763-
commentData.author_email = formData.get('email');
1775+
commentData.author_email = email || undefined;
17641776
}
17651777
// Set scroll anchor to first visible comment
17661778
this.scrollAnchor = this.findFirstVisibleComment();
@@ -1868,12 +1880,21 @@
18681880
if (loginForm) {
18691881
loginForm.addEventListener('submit', (e) => {
18701882
e.preventDefault();
1883+
1884+
// Use native HTML5 form validation
1885+
if (!loginForm.checkValidity()) {
1886+
loginForm.reportValidity();
1887+
return;
1888+
}
1889+
18711890
const formData = new FormData(loginForm);
1891+
const email = formData.get('email');
1892+
18721893
this.state.authLoading = true;
18731894
this.render();
18741895
this.sendToIframe({
18751896
action: 'login',
1876-
email: formData.get('email'),
1897+
email: email,
18771898
redirectUrl: window.location.href, // Redirect back to this page after auth
18781899
});
18791900
});
@@ -2034,15 +2055,24 @@
20342055
if (form) {
20352056
form.addEventListener('submit', (e) => {
20362057
e.preventDefault();
2058+
2059+
// Use native HTML5 form validation
2060+
if (!form.checkValidity()) {
2061+
form.reportValidity();
2062+
return;
2063+
}
2064+
20372065
const formData = new FormData(form);
2066+
const email = formData.get('email');
2067+
20382068
const commentData = {
20392069
content: formData.get('content'),
20402070
parent_id: parentId,
20412071
};
20422072
// Include guest fields only if not authenticated
20432073
if (!this.state.user) {
20442074
commentData.author_name = formData.get('name');
2045-
commentData.author_email = formData.get('email');
2075+
commentData.author_email = email || undefined;
20462076
}
20472077
// Set scroll anchor to the parent comment being replied to
20482078
this.scrollAnchor = parentId;
@@ -2284,12 +2314,21 @@
22842314
if (loginForm) {
22852315
loginForm.addEventListener('submit', (e) => {
22862316
e.preventDefault();
2317+
2318+
// Use native HTML5 form validation
2319+
if (!loginForm.checkValidity()) {
2320+
loginForm.reportValidity();
2321+
return;
2322+
}
2323+
22872324
const formData = new FormData(loginForm);
2325+
const email = formData.get('email');
2326+
22882327
this.state.authLoading = true;
22892328
this.updateModalBody();
22902329
this.sendToIframe({
22912330
action: 'login',
2292-
email: formData.get('email'),
2331+
email: email,
22932332
redirectUrl: window.location.href,
22942333
});
22952334
});
@@ -2353,12 +2392,21 @@
23532392
if (loginForm) {
23542393
loginForm.addEventListener('submit', (e) => {
23552394
e.preventDefault();
2395+
2396+
// Use native HTML5 form validation
2397+
if (!loginForm.checkValidity()) {
2398+
loginForm.reportValidity();
2399+
return;
2400+
}
2401+
23562402
const formData = new FormData(loginForm);
2403+
const email = formData.get('email');
2404+
23572405
this.state.authLoading = true;
23582406
this.updateModalBody();
23592407
this.sendToIframe({
23602408
action: 'login',
2361-
email: formData.get('email'),
2409+
email: email,
23622410
redirectUrl: window.location.href,
23632411
});
23642412
});
@@ -2379,7 +2427,7 @@
23792427
<form id="ck-modal-login-form">
23802428
<div class="ck-form-group">
23812429
<label for="ck-modal-email">Email address</label>
2382-
<input type="email" id="ck-modal-email" name="email" required placeholder="your@email.com" autocomplete="email">
2430+
<input type="email" id="ck-modal-email" name="email" required placeholder="your@email.com" autocomplete="email" pattern="[^\\s@]+@[^\\s@]+\\.[^\\s@]+" title="Please enter a valid email address (e.g., user@example.com)">
23832431
</div>
23842432
<p style="font-size: 0.875rem; color: #6b7280; margin: 0 0 28px 0; line-height: 1.6;">
23852433
We'll send you a magic link to sign in. No password needed.

frontend/src/lib/api.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,12 @@ async function request<T>(
2626
path: string,
2727
options: RequestInit = {}
2828
): Promise<ApiResponse<T>> {
29-
const token = localStorage.getItem('auth_token');
30-
3129
try {
3230
const res = await fetch(`${API_BASE}${path}`, {
3331
...options,
32+
credentials: 'include', // Include HttpOnly cookies for authentication
3433
headers: {
3534
'Content-Type': 'application/json',
36-
...(token ? { Authorization: `Bearer ${token}` } : {}),
3735
...options.headers,
3836
},
3937
});

frontend/src/lib/auth-context.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
2121
const bootstrapRef = useRef<BootstrapData | null>(null);
2222

2323
const checkAuth = async () => {
24-
const token = localStorage.getItem('auth_token');
25-
if (!token) {
26-
setLoading(false);
27-
return;
28-
}
29-
24+
// No need to check localStorage - server will read HttpOnly cookie
3025
// Request with bootstrap=true to get user + dashboard data in single call
3126
const { data, error } = await auth.me({ bootstrap: true });
3227
if (error) {
33-
localStorage.removeItem('auth_token');
28+
// User not authenticated (no valid cookie)
3429
setUser(null);
3530
} else if (data) {
3631
// Store bootstrap data for consumption
@@ -51,10 +46,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
5146
const redirectUrl = params.get('redirect');
5247

5348
if (token) {
54-
// Verify and store token
49+
// Verify token - server will set HttpOnly cookie in response
5550
auth.verify(token).then(({ data, error }) => {
5651
if (data && !error) {
57-
localStorage.setItem('auth_token', data.token);
52+
// No need to store token - server sets HttpOnly cookie
5853
setUser(data.user);
5954

6055
// Redirect to the original page if specified
@@ -82,8 +77,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
8277
};
8378

8479
const logout = async () => {
80+
// Server will clear HttpOnly cookie
8581
await auth.logout();
86-
localStorage.removeItem('auth_token');
8782
setUser(null);
8883
bootstrapRef.current = null;
8984
};

frontend/src/widget.html

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,28 @@
111111

112112
// Check for HTTP errors even with JSON response
113113
if (!response.ok) {
114-
throw new Error(data.error || data.message || `Request failed: ${response.status}`);
114+
// Handle error message properly - it might be an object (from Zod validation)
115+
let errorMessage = `Request failed: ${response.status}`;
116+
117+
if (data.error) {
118+
if (typeof data.error === 'string') {
119+
errorMessage = data.error;
120+
} else if (typeof data.error === 'object') {
121+
// Zod validation errors or other structured errors
122+
if (data.error.issues && Array.isArray(data.error.issues)) {
123+
// Zod error format
124+
errorMessage = data.error.issues.map(issue => issue.message).join(', ');
125+
} else if (data.error.message) {
126+
errorMessage = data.error.message;
127+
} else {
128+
errorMessage = JSON.stringify(data.error);
129+
}
130+
}
131+
} else if (data.message && typeof data.message === 'string') {
132+
errorMessage = data.message;
133+
}
134+
135+
throw new Error(errorMessage);
115136
}
116137

117138
return data;
@@ -217,10 +238,22 @@
217238
email: message.email
218239
}, CONFIG.parentOrigin);
219240
} catch (e) {
241+
// Ensure error message is always a string
242+
let errorMessage = 'Failed to send login email';
243+
if (e && typeof e === 'object') {
244+
if (e.message && typeof e.message === 'string') {
245+
errorMessage = e.message;
246+
} else if (e.message && typeof e.message === 'object') {
247+
errorMessage = JSON.stringify(e.message);
248+
}
249+
} else if (typeof e === 'string') {
250+
errorMessage = e;
251+
}
252+
220253
window.parent.postMessage({
221254
type: 'commentkit',
222255
action: 'error',
223-
message: e.message || 'Failed to send login email'
256+
message: errorMessage
224257
}, CONFIG.parentOrigin);
225258
}
226259
break;
@@ -235,10 +268,18 @@
235268
sendAuthState();
236269
}
237270
} catch (e) {
271+
// Ensure error message is always a string
272+
let errorMessage = 'Invalid or expired login link';
273+
if (e && typeof e === 'object' && e.message && typeof e.message === 'string') {
274+
errorMessage = e.message;
275+
} else if (typeof e === 'string') {
276+
errorMessage = e;
277+
}
278+
238279
window.parent.postMessage({
239280
type: 'commentkit',
240281
action: 'error',
241-
message: e.message || 'Invalid or expired login link'
282+
message: errorMessage
242283
}, CONFIG.parentOrigin);
243284
}
244285
break;
@@ -303,10 +344,19 @@
303344
}
304345
} catch (error) {
305346
console.error('[CommentKit Bridge] Error:', error);
347+
348+
// Ensure error message is always a string
349+
let errorMessage = 'An error occurred';
350+
if (error && typeof error === 'object' && error.message && typeof error.message === 'string') {
351+
errorMessage = error.message;
352+
} else if (typeof error === 'string') {
353+
errorMessage = error;
354+
}
355+
306356
window.parent.postMessage({
307357
type: 'commentkit',
308358
action: 'error',
309-
message: error.message || 'An error occurred'
359+
message: errorMessage
310360
}, CONFIG.parentOrigin);
311361
}
312362
});

0 commit comments

Comments
 (0)