3 Commits

Author SHA1 Message Date
dependabot[bot]
1b40f54bb2 build(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-09 00:11:16 +00:00
d386160237 Release version 2.2.2 2026-09-23 10:27:21 +02:00
7de3c74f3c fix(iframe): stop an absent iframe parameter from framing the page in itself
An empty value resolved against window.location.href, so a page without an iframe parameter passed both the scheme check and the origin allowlist carrying its own URL. Every load entered fullscreen and framed the page inside itself, and the navigation observer wrote that URL back into the parameter, nesting it deeper on every poll until the URL ran to kilometres. safeUrl now rejects a missing value, the caller tests the parameter before validating it, and the history entry keeps the URL it was given rather than the resolved one.

The regression spec covers the four properties a page without the parameter must have, and fails on each of them when the defect is put back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 10:15:52 +02:00
7 changed files with 51 additions and 8 deletions

View File

@@ -49,7 +49,7 @@ jobs:
uses: actions/checkout@v7 uses: actions/checkout@v7
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: "24" node-version: "24"
cache: npm cache: npm

View File

@@ -151,7 +151,7 @@ jobs:
run: cp app/config.sample.yaml app/config.yaml run: cp app/config.sample.yaml app/config.yaml
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: "24" node-version: "24"
cache: npm cache: npm

View File

@@ -1,5 +1,14 @@
# Changelog # Changelog
## [2.2.2] - 2026-09-23
* Vendor assets: marked 18 is found again, so the image build stops failing
* Iframe: a page without an iframe parameter no longer frames itself
* Security: the iframe and the new tab only open a URL the validator returned
* Tests: the inline-script check parses the page instead of matching a regex
* Tests: a regression spec guards the page that carries no iframe parameter
* CI: the Node jobs run the version Cypress 16 supports
## [2.2.1] - 2026-09-23 ## [2.2.1] - 2026-09-23
* Images: releases ship arm64 next to amd64, for ARM servers and Apple Silicon * Images: releases ship arm64 next to amd64, for ARM servers and Apple Silicon

View File

@@ -0,0 +1,31 @@
describe('A page without an iframe parameter', () => {
beforeEach(() => {
cy.visit('/');
});
it('stays out of fullscreen', () => {
cy.get('body').should('not.have.class', 'fullscreen');
});
it('frames nothing', () => {
cy.get('#main').find('iframe').should('not.exist');
cy.url().should('not.include', 'iframe=');
});
it('never grows an iframe parameter out of its own URL', () => {
cy.wait(2000);
cy.url().then((url) => {
expect((url.match(/iframe/g) || []).length, 'iframe parameters').to.equal(0);
});
});
it('treats an absent URL as unsafe', () => {
cy.window().then((win) => {
expect(win.safeUrl(null), 'null').to.equal(null);
expect(win.safeUrl(''), 'empty string').to.equal(null);
expect(win.safeUrl('javascript:alert(1)'), 'script URL').to.equal(null);
expect(win.safeUrl('/de/'), 'relative path').to.equal(`${win.location.origin}/de/`);
});
});
});

View File

@@ -14,8 +14,8 @@ function allowedIframeUrl(url) {
// === Auto-open iframe if URL parameter is present === // === Auto-open iframe if URL parameter is present ===
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
const paramUrl = allowedIframeUrl(new URLSearchParams(window.location.search).get('iframe')); const paramUrl = new URLSearchParams(window.location.search).get('iframe');
if (paramUrl) { if (paramUrl && allowedIframeUrl(paramUrl)) {
currentIframeUrl = paramUrl; currentIframeUrl = paramUrl;
enterFullscreen(); enterFullscreen();
openIframe(paramUrl); openIframe(paramUrl);
@@ -83,8 +83,8 @@ function openIframe(url) {
// URL-State pushen // URL-State pushen
var newUrl = new URL(window.location); var newUrl = new URL(window.location);
newUrl.searchParams.set('iframe', target); newUrl.searchParams.set('iframe', url);
window.history.pushState({ iframe: target }, '', newUrl); window.history.pushState({ iframe: url }, '', newUrl);
}); });
} }

View File

@@ -8,8 +8,11 @@ function t(source) {
const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:']; const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:'];
function safeUrl(url) { function safeUrl(url) {
if (url == null || String(url) === '') {
return null;
}
try { try {
const parsed = new URL(String(url == null ? '' : url), window.location.href); const parsed = new URL(String(url), window.location.href);
return SAFE_URL_SCHEMES.includes(parsed.protocol) ? parsed.href : null; return SAFE_URL_SCHEMES.includes(parsed.protocol) ? parsed.href : null;
} catch (error) { } catch (error) {
return null; return null;

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "portfolio-ui" name = "portfolio-ui"
version = "2.2.1" version = "2.2.2"
description = "A lightweight YAML-driven portfolio and landing-page generator." description = "A lightweight YAML-driven portfolio and landing-page generator."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"