mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-09-23 19:03:18 +00:00
Compare commits
5 Commits
v2.2.1
...
fix/iframe
| Author | SHA1 | Date | |
|---|---|---|---|
| 7de3c74f3c | |||
| 7353e8d96a | |||
| 4cc8052a88 | |||
| 87c4405aa4 | |||
| ff6e7d3689 |
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
cache-dependency-path: app/package.json
|
||||
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -153,7 +153,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
cache-dependency-path: app/package.json
|
||||
|
||||
|
||||
31
app/cypress/e2e/iframe_param_absent.spec.js
Normal file
31
app/cypress/e2e/iframe_param_absent.spec.js
Normal 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/`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ const SHARED = {
|
||||
openDynamicPopup: 'readonly',
|
||||
closeAllModals: 'readonly',
|
||||
isSafeUrl: 'readonly',
|
||||
safeUrl: 'readonly',
|
||||
openIframe: 'readonly',
|
||||
enterFullscreen: 'readonly',
|
||||
exitFullscreen: 'readonly',
|
||||
|
||||
@@ -63,6 +63,7 @@ const markedCandidates = [
|
||||
path.join(NM, 'marked', 'marked.min.js'), // v4.x
|
||||
path.join(NM, 'marked', 'lib', 'marked.umd.min.js'), // v5.x
|
||||
path.join(NM, 'marked', 'dist', 'marked.min.js'), // v9+
|
||||
path.join(NM, 'marked', 'lib', 'marked.umd.js'), // v16+
|
||||
];
|
||||
const markedSrc = markedCandidates.find(p => fs.existsSync(p));
|
||||
if (!markedSrc) throw new Error('marked: no browser UMD build found in node_modules');
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
let mainElement, originalContent, originalMainStyle, container, customScrollbar, scrollbarContainer;
|
||||
let currentIframeUrl = null;
|
||||
|
||||
function isAllowedIframeUrl(url) {
|
||||
if (!isSafeUrl(url)) {
|
||||
return false;
|
||||
function allowedIframeUrl(url) {
|
||||
const candidate = safeUrl(url);
|
||||
if (candidate === null) {
|
||||
return null;
|
||||
}
|
||||
const allowedOrigins = new Set([window.location.origin]);
|
||||
document.querySelectorAll('a.iframe-link[href]').forEach((link) => allowedOrigins.add(link.origin));
|
||||
return allowedOrigins.has(new URL(url, window.location.href).origin);
|
||||
return allowedOrigins.has(new URL(candidate).origin) ? candidate : null;
|
||||
}
|
||||
|
||||
// === Auto-open iframe if URL parameter is present ===
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const paramUrl = new URLSearchParams(window.location.search).get('iframe');
|
||||
if (paramUrl && isAllowedIframeUrl(paramUrl)) {
|
||||
if (paramUrl && allowedIframeUrl(paramUrl)) {
|
||||
currentIframeUrl = paramUrl;
|
||||
enterFullscreen();
|
||||
openIframe(paramUrl);
|
||||
@@ -43,7 +44,8 @@ function syncIframeHeight() {
|
||||
|
||||
// Function to open a URL in an iframe (jQuery version mit 1500 ms Fade)
|
||||
function openIframe(url) {
|
||||
if (!isSafeUrl(url)) {
|
||||
const target = safeUrl(url);
|
||||
if (target === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ function openIframe(url) {
|
||||
|
||||
// Quelle setzen und mit 1500 ms einblenden
|
||||
$iframe
|
||||
.attr('src', url)
|
||||
.attr('src', target)
|
||||
.fadeIn(1500, function() {
|
||||
syncIframeHeight();
|
||||
observeIframeNavigation();
|
||||
@@ -148,8 +150,8 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
*/
|
||||
function openIframeInNewTab() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const iframeUrl = params.get('iframe');
|
||||
if (iframeUrl && isAllowedIframeUrl(iframeUrl)) {
|
||||
const iframeUrl = allowedIframeUrl(params.get('iframe'));
|
||||
if (iframeUrl) {
|
||||
window.open(iframeUrl, '_blank');
|
||||
} else {
|
||||
alert('No iframe is currently open.');
|
||||
|
||||
@@ -7,13 +7,20 @@ function t(source) {
|
||||
|
||||
const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:'];
|
||||
|
||||
function isSafeUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url == null ? '' : url), window.location.href);
|
||||
return SAFE_URL_SCHEMES.includes(parsed.protocol);
|
||||
} catch (error) {
|
||||
return false;
|
||||
function safeUrl(url) {
|
||||
if (url == null || String(url) === '') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(String(url), window.location.href);
|
||||
return SAFE_URL_SCHEMES.includes(parsed.protocol) ? parsed.href : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isSafeUrl(url) {
|
||||
return safeUrl(url) !== null;
|
||||
}
|
||||
|
||||
function iconAndName(item) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
@@ -112,18 +113,29 @@ class TestEscaping(AppRouteMixin, unittest.TestCase):
|
||||
self.assertIn("<script>alert('config')", body)
|
||||
|
||||
|
||||
class InlineScriptCollector(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.inline = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag != "script":
|
||||
return
|
||||
attributes = dict(attrs)
|
||||
if "src" in attributes or attributes.get("type") == "application/json":
|
||||
return
|
||||
self.inline.append(self.get_starttag_text())
|
||||
|
||||
|
||||
class TestContentSecurityPolicy(AppRouteMixin, unittest.TestCase):
|
||||
def test_page_ships_no_executable_inline_script(self):
|
||||
body = self.client.get("/de/").get_data(as_text=True)
|
||||
|
||||
inline = [
|
||||
tag
|
||||
for tag in re.findall(r"<script\b[^>]*>", body)
|
||||
if "src=" not in tag and 'type="application/json"' not in tag
|
||||
]
|
||||
collector = InlineScriptCollector()
|
||||
collector.feed(body)
|
||||
|
||||
self.assertEqual(
|
||||
inline,
|
||||
collector.inline,
|
||||
[],
|
||||
"a host CSP can only hash an inline script whose content it knows, "
|
||||
"and this one changes with every language",
|
||||
|
||||
@@ -2,8 +2,8 @@ import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -170,7 +170,7 @@ class TestCatalogMerge(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_an_unsupported_code_never_becomes_a_path(self):
|
||||
with mock.patch.object(i18n, "read_catalog") as read:
|
||||
with unittest.mock.patch.object(i18n, "read_catalog") as read:
|
||||
self.assertEqual(i18n.catalog("../content/de"), {})
|
||||
|
||||
read.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user