Skip to content

Commit 3074eb7

Browse files
authored
fix: Cross-Site Scripting (XSS) via HTML pages for password reset and email verification [GHSA-jhgf-2h8h-ggxv](GHSA-jhgf-2h8h-ggxv) (#9985)
1 parent 7028e03 commit 3074eb7

File tree

7 files changed

+89
-21
lines changed

7 files changed

+89
-21
lines changed

public/de-AT/email_verification_link_expired.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
<body>
1515
<h1>{{appName}}</h1>
1616
<h1>Expired verification link!</h1>
17-
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
18-
<input name="token" type="hidden" value="{{{token}}}">
19-
<input name="locale" type="hidden" value="{{{locale}}}">
17+
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
18+
<input name="token" type="hidden" value="{{token}}">
19+
<input name="locale" type="hidden" value="{{locale}}">
2020
<button type="submit">Resend Link</button>
2121
</form>
2222
</body>

public/de-AT/password_reset.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ <h1>Reset Your Password</h1>
2323
<p>You can set a new Password for your account: {{username}}</p>
2424
<br />
2525
<p>{{error}}</p>
26-
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
26+
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
2727
<input name='utf-8' type='hidden' value='' />
28-
<input name="username" type="hidden" id="username" value="{{{username}}}" />
29-
<input name="token" type="hidden" id="token" value="{{{token}}}" />
30-
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
28+
<input name="username" type="hidden" id="username" value="{{username}}" />
29+
<input name="token" type="hidden" id="token" value="{{token}}" />
30+
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
3131

3232
<p>New Password</p>
3333
<input name="new_password" type="password" id="password" />

public/de/email_verification_link_expired.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
<body>
1515
<h1>{{appName}}</h1>
1616
<h1>Expired verification link!</h1>
17-
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
18-
<input name="token" type="hidden" value="{{{token}}}">
19-
<input name="locale" type="hidden" value="{{{locale}}}">
17+
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
18+
<input name="token" type="hidden" value="{{token}}">
19+
<input name="locale" type="hidden" value="{{locale}}">
2020
<button type="submit">Resend Link</button>
2121
</form>
2222
</body>

public/de/password_reset.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ <h1>Reset Your Password</h1>
2323
<p>You can set a new Password for your account: {{username}}</p>
2424
<br />
2525
<p>{{error}}</p>
26-
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
26+
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
2727
<input name='utf-8' type='hidden' value='' />
28-
<input name="username" type="hidden" id="username" value="{{{username}}}" />
29-
<input name="token" type="hidden" id="token" value="{{{token}}}" />
30-
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
28+
<input name="username" type="hidden" id="username" value="{{username}}" />
29+
<input name="token" type="hidden" id="token" value="{{token}}" />
30+
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
3131

3232
<p>New Password</p>
3333
<input name="new_password" type="password" id="password" />

public/email_verification_link_expired.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
<body>
1515
<h1>{{appName}}</h1>
1616
<h1>Expired verification link!</h1>
17-
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
18-
<input name="token" type="hidden" value="{{{token}}}">
19-
<input name="locale" type="hidden" value="{{{locale}}}">
17+
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
18+
<input name="token" type="hidden" value="{{token}}">
19+
<input name="locale" type="hidden" value="{{locale}}">
2020
<button type="submit">Resend Link</button>
2121
</form>
2222
</body>

public/password_reset.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ <h1>Reset Your Password</h1>
2323
<p>You can set a new Password for your account: {{username}}</p>
2424
<br />
2525
<p>{{error}}</p>
26-
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
26+
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
2727
<input name='utf-8' type='hidden' value='' />
28-
<input name="username" type="hidden" id="username" value="{{{username}}}" />
29-
<input name="token" type="hidden" id="token" value="{{{token}}}" />
30-
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
28+
<input name="username" type="hidden" id="username" value="{{username}}" />
29+
<input name="token" type="hidden" id="token" value="{{token}}" />
30+
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
3131

3232
<p>New Password</p>
3333
<input name="new_password" type="password" id="password" />

spec/PagesRouter.spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,4 +1180,72 @@ describe('Pages Router', () => {
11801180
});
11811181
});
11821182
});
1183+
1184+
describe('XSS Protection', () => {
1185+
beforeEach(async () => {
1186+
await reconfigureServer({
1187+
appId: 'test',
1188+
appName: 'exampleAppname',
1189+
publicServerURL: 'http://localhost:8378/1',
1190+
pages: { enableRouter: true },
1191+
});
1192+
});
1193+
1194+
it('should escape XSS payloads in token parameter', async () => {
1195+
const xssPayload = '"><script>alert("XSS")</script>';
1196+
const response = await request({
1197+
url: `http://localhost:8378/1/apps/choose_password?token=${encodeURIComponent(xssPayload)}&username=test&appId=test`,
1198+
});
1199+
1200+
expect(response.status).toBe(200);
1201+
expect(response.text).not.toContain('<script>alert("XSS")</script>');
1202+
expect(response.text).toContain('&quot;&gt;&lt;script&gt;');
1203+
});
1204+
1205+
it('should escape XSS in username parameter', async () => {
1206+
const xssUsername = '<img src=x onerror=alert(1)>';
1207+
const response = await request({
1208+
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(xssUsername)}&appId=test`,
1209+
});
1210+
1211+
expect(response.status).toBe(200);
1212+
expect(response.text).not.toContain('<img src=x onerror=alert(1)>');
1213+
expect(response.text).toContain('&lt;img');
1214+
});
1215+
1216+
it('should escape XSS in locale parameter', async () => {
1217+
const xssLocale = '"><svg/onload=alert(1)>';
1218+
const response = await request({
1219+
url: `http://localhost:8378/1/apps/choose_password?locale=${encodeURIComponent(xssLocale)}&appId=test`,
1220+
});
1221+
1222+
expect(response.status).toBe(200);
1223+
expect(response.text).not.toContain('<svg/onload=alert(1)>');
1224+
expect(response.text).toContain('&quot;&gt;&lt;svg');
1225+
});
1226+
1227+
it('should handle legitimate usernames with quotes correctly', async () => {
1228+
const username = "O'Brien";
1229+
const response = await request({
1230+
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(username)}&appId=test`,
1231+
});
1232+
1233+
expect(response.status).toBe(200);
1234+
// Should be properly escaped as HTML entity
1235+
expect(response.text).toContain('O&#39;Brien');
1236+
// Should NOT contain unescaped quote that breaks HTML
1237+
expect(response.text).not.toContain('value="O\'Brien"');
1238+
});
1239+
1240+
it('should handle legitimate usernames with ampersands correctly', async () => {
1241+
const username = 'Smith & Co';
1242+
const response = await request({
1243+
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(username)}&appId=test`,
1244+
});
1245+
1246+
expect(response.status).toBe(200);
1247+
// Should be properly escaped
1248+
expect(response.text).toContain('Smith &amp; Co');
1249+
});
1250+
});
11831251
});

0 commit comments

Comments
 (0)