From db81de90073d4bf136d43051ea508350f3970c14 Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Sat, 16 May 2026 13:59:10 +0200 Subject: [PATCH] =?UTF-8?q?Bump=20to=20v0.12.16=20=E2=80=94=20security=20h?= =?UTF-8?q?ardening:=20http(s)=20scheme=20guard=20on=20link=20sinks,=20loo?= =?UTF-8?q?p-stable=20HTML=20tag=20strip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 7 +++++++ PKGBUILD | 2 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/components/feed/FountainCard.tsx | 3 ++- src/components/feed/MediaCards.tsx | 9 ++++---- src/components/feed/TextSegments.tsx | 3 ++- src/components/podcast/EpisodeList.tsx | 3 ++- src/components/podcast/PodcastCard.tsx | 3 ++- src/lib/utils.ts | 29 ++++++++++++++++++++++++++ 12 files changed, 54 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 762bcc1..65017dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,6 +69,13 @@ jobs: > **Windows note:** The installer is not yet code-signed. Windows SmartScreen will show an "Unknown publisher" warning — click "More info → Run anyway" to install. + ### v0.12.16 — Security hardening + + A defense-in-depth pass on link and text rendering, prompted by a CodeQL code-scanning review. No user-facing changes, and no exploitable vulnerability was found — note URLs were already scheme-constrained upstream — but the rendering sinks are now hardened directly. + + - **External links** now route every `href` through a scheme guard that permits only `http(s)://`. A `javascript:` or `data:` URI in note content can never reach a clickable link, even if the content parser changes in future. + - **Podcast description text** is now stripped of HTML tags with a loop-until-stable pass, so split or nested tags can't survive sanitization. + ### v0.12.15 — Honest update banner on Linux The "Update & restart" banner now adapts to how Vega was installed. On a package-manager install — the AUR `vega-nostr-git` package, or a `.deb`/`.rpm` — the in-app updater can't replace a root-owned binary under `/usr`, so the button did nothing. The banner now detects the install kind and shows the right path: diff --git a/PKGBUILD b/PKGBUILD index 9c19eee..15d6d92 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: hoornet pkgname=vega-nostr -pkgver=0.12.15 +pkgver=0.12.16 pkgrel=1 pkgdesc="Cross-platform Nostr desktop client with Lightning integration" arch=('x86_64') diff --git a/package.json b/package.json index 0a487d0..034a31c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vega", "private": true, - "version": "0.12.15", + "version": "0.12.16", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0ba523b..0342c14 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5429,7 +5429,7 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vega" -version = "0.12.15" +version = "0.12.16" dependencies = [ "futures-util", "hex", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 82b0caf..fe8cbc8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vega" -version = "0.12.15" +version = "0.12.16" description = "Cross-platform Nostr desktop client with Lightning integration" authors = ["hoornet"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e3125bf..4bdca75 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Vega", - "version": "0.12.15", + "version": "0.12.16", "identifier": "com.hoornet.vega", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/feed/FountainCard.tsx b/src/components/feed/FountainCard.tsx index 126ddcc..846ded9 100644 --- a/src/components/feed/FountainCard.tsx +++ b/src/components/feed/FountainCard.tsx @@ -3,6 +3,7 @@ import type { ContentSegment } from "../../lib/parsing"; import type { PodcastEpisode } from "../../types/podcast"; import { resolveFountainEpisode } from "../../lib/podcast"; import { usePodcastStore } from "../../stores/podcast"; +import { safeHttpUrl } from "../../lib/utils"; export function FountainCard({ seg }: { seg: ContentSegment }) { const [episode, setEpisode] = useState(null); @@ -22,7 +23,7 @@ export function FountainCard({ seg }: { seg: ContentSegment }) { // Fallback: render as a regular link return ( s.isSubscribed(show.feedUrl)); @@ -87,7 +88,7 @@ export function EpisodeList({ show, onBack }: EpisodeListProps) { {show.description && (
- {show.description.replace(/<[^>]+>/g, "").slice(0, 200)} + {stripHtmlTags(show.description).slice(0, 200)}
)} diff --git a/src/components/podcast/PodcastCard.tsx b/src/components/podcast/PodcastCard.tsx index 0ff2add..10438b7 100644 --- a/src/components/podcast/PodcastCard.tsx +++ b/src/components/podcast/PodcastCard.tsx @@ -1,5 +1,6 @@ import type { PodcastShow } from "../../types/podcast"; import { usePodcastStore } from "../../stores/podcast"; +import { stripHtmlTags } from "../../lib/utils"; interface PodcastCardProps { show: PodcastShow; @@ -32,7 +33,7 @@ export function PodcastCard({ show, onClick }: PodcastCardProps) {
{show.author}
{show.description && (
- {show.description.replace(/<[^>]+>/g, "").slice(0, 120)} + {stripHtmlTags(show.description).slice(0, 120)}
)} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 49900ad..4cfcdbd 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -17,3 +17,32 @@ export function profileName(profile: any, fallback: string): string { const raw = profile?.displayName || profile?.name; return (typeof raw === "string" ? raw : null) || fallback; } + +/** + * Returns `url` only if it uses a safe http(s) scheme, otherwise "#". + * Defense-in-depth for `href` sinks: blocks `javascript:`/`data:` URIs even if + * a future change to the content parser stops scheme-constraining them upstream. + */ +export function safeHttpUrl(url: string): string { + try { + const scheme = new URL(url).protocol; + return scheme === "http:" || scheme === "https:" ? url : "#"; + } catch { + return "#"; + } +} + +/** + * Strips HTML tags, looping until the result is stable so split/nested tags + * like `ipt>` cannot survive a single pass. Used to flatten + * HTML-formatted text (e.g. podcast feed descriptions) into plain text. + */ +export function stripHtmlTags(input: string): string { + let prev: string; + let s = input; + do { + prev = s; + s = s.replace(/<[^>]*>/g, ""); + } while (s !== prev); + return s; +}