Merge pull request #522 from MBombeck/release/v1.29.5

release/v1.29.5
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-17 17:47:16 +02:00
commit 49204f98a2
6 files changed
+53 -5

No files matched your search

+22
View File
@@ -2,6 +2,28 @@
## [Unreleased]
## [1.29.5] — 2026-07-17
### Security
- **Hardened the same-origin redirect guard.** A crafted relative
post-login `next` path (e.g. `/..//evil.com`) passed the same-origin
check but left a protocol-relative pathname that could re-resolve to a
foreign origin when re-used in a redirect or client navigation. The
guard now re-verifies the reconstructed path resolves on-origin and
rejects it otherwise. Covers the OIDC callback and the password/passkey
login redirect.
### Fixed
- **OIDC login redirects use the operator app URL, not the request URL.**
Behind a reverse proxy that does not forward the original host, the
login and callback routes built their browser redirects from the
address the app was reached at inside the container, sending users to
an unreachable internal URL after login. Every OIDC redirect is now
built from the operator-set `NEXT_PUBLIC_APP_URL`, matching the
convention the other integrations already use.
## [1.29.4] — 2026-07-17
- **The workouts list shows the most recent first.** The list was ordered
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.29.4
version: 1.29.5
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.29.4",
"version": "1.29.5",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
+1 -1
View File
@@ -36,7 +36,7 @@ try {
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
/* @sw-version-fallback */ "v1.29.4";
/* @sw-version-fallback */ "v1.29.5";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
+17
View File
@@ -31,6 +31,23 @@ describe("sanitizeSameOriginPath", () => {
expect(sanitizeSameOriginPath("\\\\evil.com", BASE)).toBe("/");
});
it("rejects a same-origin next whose pathname collapses to protocol-relative", () => {
// `/..//evil.com` resolves ON-origin here (the `..` collapses at root),
// but its pathname is `//evil.com`. Consumers re-resolve the returned
// value (`new URL(result, base)`, `router.push`), where `//evil.com`
// becomes protocol-relative and escapes to https://evil.com. The
// reconstructed-path re-check must reject these to "/".
expect(sanitizeSameOriginPath("/..//evil.com", BASE)).toBe("/");
expect(sanitizeSameOriginPath("/.//evil.com", BASE)).toBe("/");
expect(sanitizeSameOriginPath("/../..//evil.com", BASE)).toBe("/");
});
it("still allows a legitimate same-origin path with an inner double slash", () => {
// Only a LEADING `//` (protocol-relative) escapes on re-resolve; a double
// slash inside the path stays on-origin and must be preserved.
expect(sanitizeSameOriginPath("/reports/a//b", BASE)).toBe("/reports/a//b");
});
it("preserves a same-origin path's search and hash", () => {
expect(sanitizeSameOriginPath("/settings?tab=security#top", BASE)).toBe(
"/settings?tab=security#top",
+11 -2
View File
@@ -22,10 +22,19 @@ export function sanitizeSameOriginPath(
): string {
if (!next) return "/";
try {
const resolved = new URL(next, baseUrl);
const base = new URL(baseUrl);
const resolved = new URL(next, baseUrl);
if (resolved.origin !== base.origin) return "/";
return `${resolved.pathname}${resolved.search}${resolved.hash}`;
const result = `${resolved.pathname}${resolved.search}${resolved.hash}`;
// Consumers commonly RE-RESOLVE the returned value against the origin
// (`new URL(result, base)` in the OIDC callback redirect, `router.push()`
// on the login page). A same-origin `next` can still leave a pathname
// like `//evil.com` — e.g. `/..//evil.com` resolves on-origin here (the
// `..` collapses at root) yet its pathname is protocol-relative, so the
// re-resolve escapes to `https://evil.com`. Re-verify that the
// reconstructed path itself still resolves on-origin; reject otherwise.
if (new URL(result, base).origin !== base.origin) return "/";
return result;
} catch {
return "/";
}