Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/scripts/check-document-filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Check that render() in dist/app.js still drops every element that could act
on MemoryStick's own page.

comrak keeps a document's raw HTML as written, and the inert <template> pass
in render() is the only place these elements are removed. This fails if one
of them is missing from DROPPED_ELEMENTS, or if render() stops using it.
"""

import re
import sys
from pathlib import Path

REQUIRED = {"meta", "link", "iframe", "frame", "frameset", "object", "embed", "script", "base", "style"}
# Ordinary raw HTML a document is allowed to keep, as on GitHub.
KEPT = {"kbd", "details", "summary", "sub", "sup", "svg", "img", "a"}

app = (Path(__file__).resolve().parents[2] / "dist" / "app.js").read_text(encoding="utf-8")

errors = []
match = re.search(r"^const DROPPED_ELEMENTS = '([^']*)';$", app, flags=re.M)
if not match:
errors.append("dist/app.js: no `const DROPPED_ELEMENTS = '...';` line")
else:
dropped = {name.strip() for name in match.group(1).split(",")}
if missing := REQUIRED - dropped:
errors.append(f"DROPPED_ELEMENTS no longer drops: {', '.join(sorted(missing))}")
if wrongly := KEPT & dropped:
errors.append(f"DROPPED_ELEMENTS drops ordinary HTML: {', '.join(sorted(wrongly))}")
if ".querySelectorAll(DROPPED_ELEMENTS).forEach((el) => el.remove());" not in app:
errors.append("dist/app.js: render() no longer removes DROPPED_ELEMENTS from the document")

print("\n".join(errors) or f"OK: render() drops all {len(REQUIRED)} elements")
sys.exit(1 if errors else 0)
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,21 @@ jobs:

- name: Every theme defines the colours dist/ uses
run: python3 .github/scripts/check-colony-themes.py

# render() in dist/app.js is the only place a document's scripts, frames
# and style sheets are removed (comrak keeps raw HTML as written).
document-filter:
name: Document filter
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: .

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: render() drops every element that could act on the page
run: python3 .github/scripts/check-document-filter.py
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ any other file, or a network share by its address, such as
`\\server\share` on Windows. It does not upload, copy or keep these
files.

Documents can contain HTML, which is displayed, but no script contained in a
document is ever run. The window always shows MemoryStick's own page: nothing
Documents can contain HTML, which is displayed, apart from scripts, frames,
embedded objects and style sheets, which are left out: no script contained in
a document is ever run. The window always shows MemoryStick's own page: nothing
in a document can navigate it to another page or file. Web and email links
open in your default browser or mail app, and only when you click them;
links to a place in the document scroll to it, and other links do nothing.
Expand Down
18 changes: 13 additions & 5 deletions dist/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,19 +304,27 @@ document.addEventListener('click', (e) => {
}, true);

// ===== Rendering =====
// The elements render() drops from a document. .github/scripts/
// check-document-filter.py fails CI if one of them goes missing.
const DROPPED_ELEMENTS = 'meta, link, iframe, frame, frameset, object, embed, script, base, style';

// The page never names a file: main.rs reads only the document the user
// opened, through the Open dialog, a drop or the command line.
function render(result) {
currentFilePath = result.file_path;
currentDir = dirname(currentFilePath);

// Parsed in an inert <template> first, so that the document's <meta>
// elements (a refresh tag navigates the window) and <link> elements
// (preconnect and prefetch hints reach the network) are dropped before
// the page ever sees them.
// Parsed in an inert <template> first, so that the elements a document
// could use to act on MemoryStick's own page are dropped before the page
// ever sees them: <meta> (a refresh tag navigates the window), <link>
// (preconnect and prefetch hints reach the network), frames, <object> and
// <embed> (an <iframe srcdoc> shares the page's origin and could load its
// scripts), <script>, <base> (it would move every relative link) and
// <style> (its rules would restyle the interface, not only the document).
// comrak keeps raw HTML as it is, so this is the one place they are removed.
const doc = document.createElement('template');
doc.innerHTML = result.html;
doc.content.querySelectorAll('meta, link').forEach((el) => el.remove());
doc.content.querySelectorAll(DROPPED_ELEMENTS).forEach((el) => el.remove());
$content.replaceChildren(doc.content);
$main.classList.add('has-content');
// A document takes the window back from Preferences.
Expand Down
4 changes: 4 additions & 0 deletions dist/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,14 @@ body.drag-over #drop-hint .hint-title { color: var(--colony-accent-blue); }

/* ===== Markdown content =====
.markdown-body is the document, and the preview in Preferences. */
/* contain: paint keeps a document inside its own box: an element with an
inline position: fixed is placed and clipped within #content, so it
cannot be drawn over the toolbar or the rest of the interface. */
#content {
max-width: 920px;
margin: 0 auto;
padding: 40px 60px 100px;
contain: paint;
}
.markdown-body {
line-height: 1.65;
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pub fn render(markdown: &str) -> String {
ext.header_id_prefix = Some("h-".to_string());
ext.front_matter_delimiter = Some("---".to_string());
opts.parse.smart = true;
// Raw HTML is kept as written (kbd, details, sub...). render() in app.js
// drops the elements that could act on the app's own page, such as
// scripts, frames and style sheets, before the document is shown.
opts.render.r#unsafe = true;

markdown_to_html(markdown, &opts)
Expand Down
Loading