fix: URL-encode pad names in admin 'Open' button and recent pads (#7865) (#7895)

* fix: URL-encode pad names in admin 'Open' button and recent pads (#7865)

- encodeURIComponent in admin PadPage 'Open' button href
- decodeURIComponent when reading pad name from URL pathname
  in pad_userlist.ts and colibris/pad.js (recent pads storage)
- encodeURIComponent in colibris/index.js recent pads href;
  display text uses stored name directly (no double-decode)
- add recent_pads spec asserting encoded URLs
- add share dialog spec asserting URL encoding of special chars

* fix: address Qodo review on recent-pads encoding and admin Open button

- Normalize legacy URL-encoded recentPads names before re-encoding the
  href in colibris/index.js, preventing double-encoding (%2F -> %252F)
  of entries stored by older versions.
- Add noopener,noreferrer to the admin 'Open' window.open call to
  prevent reverse tabnabbing, matching the pattern used elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-06-05 14:02:35 +01:00 committed by GitHub
parent 3e6f9d7bb2
commit 189ea4a29b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 57 additions and 5 deletions

View file

@ -418,7 +418,7 @@ export const PadPage = () => {
</button>
<button
className="pm-btn pm-btn-primary pm-btn--sm"
onClick={() => window.open(`../../p/${pad.padName}`, '_blank')}
onClick={() => window.open(`../../p/${encodeURIComponent(pad.padName)}`, '_blank', 'noopener,noreferrer')}
>
<Eye size={13}/> <Trans i18nKey="admin_pads.open"/>
</button>

View file

@ -557,7 +557,7 @@ const paduserlist = (() => {
if (localStorage.getItem('recentPads') != null) {
const recentPadsList = JSON.parse(localStorage.getItem('recentPads'));
const pathSegments = window.location.pathname.split('/');
const padName = pathSegments[pathSegments.length - 1];
const padName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
const existingPad = recentPadsList.find((pad) => pad.name === padName);
if (existingPad) {
existingPad.members = online;

View file

@ -70,12 +70,21 @@ window.customStart = () => {
li.style.cursor = 'pointer';
li.className = 'recent-pad';
const padPath = `${window.location.href}p/${pad.name}`;
// Normalize the stored name to its decoded form. Entries saved by older
// versions may already be URL-encoded; decoding first avoids
// double-encoding (e.g. %2F -> %252F) when we build the href below.
let padName = pad.name;
try {
padName = decodeURIComponent(pad.name);
} catch {
// pad.name is not valid percent-encoding; use it as-is.
}
const padPath = `${window.location.href}p/${encodeURIComponent(padName)}`;
const link = document.createElement('a');
link.style.textDecoration = 'none';
link.href = padPath;
link.innerText = pad.name;
link.innerText = padName;
li.appendChild(link);

View file

@ -8,7 +8,7 @@ window.customStart = () => {
$('.buttonicon').on('mouseup', function () { $(this).parent().removeClass('pressed'); });
const pathSegments = window.location.pathname.split('/');
const padName = pathSegments[pathSegments.length - 1];
const padName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
const recentPads = localStorage.getItem('recentPads');
if (recentPads == null) {
localStorage.setItem('recentPads', JSON.stringify([]));

View file

@ -133,4 +133,17 @@ test.describe('embed links', function () {
await checkiFrameCode(embedCode, true, page);
});
})
test.describe('URL encoding of pad names', function () {
test('share link encodes special characters in pad name', async function ({page}) {
const padName = 'test%2Fencoding';
await page.goto(`http://localhost:9001/p/${padName}`);
const shareButton = page.locator('.buttonicon-embed');
await shareButton.click();
const shareLink = await page.locator('#linkinput').inputValue();
expect(shareLink).toContain(padName);
});
})
})

View file

@ -0,0 +1,30 @@
import { test, expect } from '@playwright/test';
test.describe('Recent Pads', () => {
test('should display correctly encoded URLs for recent pads', async ({ page }) => {
const padName = 'test pad with spaces & / chars';
const recentPads = [
{
name: padName,
timestamp: new Date().toISOString(),
members: 1,
},
];
await page.addInitScript((data) => {
window.localStorage.setItem('recentPads', data);
}, JSON.stringify(recentPads));
await page.goto('http://localhost:9001/');
const recentPad = page.locator('.recent-pad').first();
await expect(recentPad).toBeVisible();
const link = recentPad.locator('a');
await expect(link).toHaveText(padName);
const expectedEncodedName = encodeURIComponent(padName);
const expectedHrefRegex = new RegExp(`p/${expectedEncodedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
await expect(link).toHaveAttribute('href', expectedHrefRegex);
});
});