[codex] Mark watch viewed when opening page link#4253
Conversation
dgtlmoon
left a comment
There was a problem hiding this comment.
Nice fix for the last_viewed problem — routing through an internal endpoint is the right call. One security note on the redirect, though.
rel="noopener" does nothing for the Referer header. It only severs window.opener (reverse-tabnabbing). The token that strips referer is rel="noreferrer". So the retained noopener doesn't protect anything here.
The redirect makes the watch UUID the referer source. Before this PR the link went straight to watch.link, so the Referer the monitored site saw was the overview page (https://cd-host/) — no UUID. Now the browser first hits /form/watch/<uuid>/open and that URL becomes the referer source for the 302 to the external site. So the per-watch UUID can now leak to the monitored (potentially adversarial) site.
Whether it actually leaks depends on the effective Referrer-Policy:
- The app only sets
Referrer-Policy: same-originwhenHIDE_REFERER=true, which is off by default (changedetectionio/__init__.py). - Modern browser default (
strict-origin-when-cross-origin): only the origin leaks (path/UUID stripped) — same as before. - Legacy default (
no-referrer-when-downgrade) orunsafe-url(older browsers / some proxies): the full URL incl. the UUID leaks on HTTPS→HTTPS.
Leaking the UUID tells the monitored site it's being watched and hands over the stable internal watch ID (used in edit/diff/preview/API/RSS URLs) — an info-disclosure the pre-PR direct link didn't have.
Suggested fix — set the policy on the redirect response itself, so it's independent of global config and browser defaults (a redirect's Referrer-Policy governs the follow-up request):
target_url = watch.link
try:
datastore.set_last_viewed(uuid, int(time.time()))
except Exception as e:
# Opening the monitored page should still work if viewed state cannot be saved.
logger.error(f"Error marking watch {uuid} as viewed: {e}")
resp = redirect(target_url)
resp.headers["Referrer-Policy"] = "no-referrer"
return respOptionally also make the anchor rel="noopener noreferrer" as defense-in-depth, but the server-side header is the authoritative fix and doesn't rely on the template.
|
Addressed in |
Fixes #4218
What changed
target="_blank"andrel="noopener"behavior, so normal clicks, modifier-clicks, middle-clicks, and keyboard activation retain native browser semantics.Root cause
The button linked directly to the monitored URL, so changedetection.io never received a request and could not update the watch's
last_viewedtimestamp. The new endpoint reuses the sameset_last_viewedpath used by existing viewed actions, including the realtime watch update signal.Testing
FLASK_SERVER_NAME=localhost PYTHONPATH=. pytest -q changedetectionio/tests/test_ui.py --tb=short— 6 passedruff check . --select E9,F63,F7,F82,INT— passedgit diff --check— passed