mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
6 files changed
+53
-5
No files matched your search
@@ -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,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
@@ -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
@@ -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/*`
|
||||
|
||||
@@ -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
@@ -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 "/";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user