Skip to content

Supplements: save product URL for future reference#236

Open
Savi-1 wants to merge 1 commit into
elkimek:mainfrom
Savi-1:supplements-save-source-url
Open

Supplements: save product URL for future reference#236
Savi-1 wants to merge 1 commit into
elkimek:mainfrom
Savi-1:supplements-save-source-url

Conversation

@Savi-1
Copy link
Copy Markdown
Contributor

@Savi-1 Savi-1 commented May 15, 2026

Summary

  • Persist the URL pasted into the supplement editor as entry.sourceUrl so it survives saves and re-opens.
  • Show a small hostname ↗ link in the supplement list next to the date range (opens in new tab; click doesn't toggle the accordion).
  • URL field is now visible even without an AI provider configured (the Fetch auto-fill button stays AI-gated).
  • Scheme-validated (http/https only) before save to block javascript: / data: URLs.

sourceUrl is just a new optional string field on supplement entries — no migration, no schema bump. Per-row CRDT sync picks it up automatically since supplements is in DELTA_ARRAYS.

Test plan

  • Add a supplement, paste a URL, save → reopen → URL is still there
  • Saved supplement row shows hostname ↗ link; clicking the link opens the page in a new tab without expanding the accordion
  • Invalid URL (e.g. not-a-url or javascript:alert(1)) is rejected at save with an error notification
  • Form renders correctly with no AI provider configured (URL input visible, Fetch button hidden)
  • Existing supplements with no sourceUrl render as before (no broken link)

Persists the URL pasted into the supplement editor as `entry.sourceUrl`
so users can re-open the source page later. URL field is now visible
even without an AI provider (Fetch button stays AI-gated). List rows
show a hostname link next to the date range.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

@Savi-1 is attempting to deploy a commit to the elkimek's projects Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 15, 2026

Greptile Summary

This PR persists the product URL entered in the supplement editor as entry.sourceUrl, shows a hostname ↗ link in the supplement list, and makes the URL field always visible (with the Fetch button still AI-gated). The save path correctly validates the scheme (http:/https: only), but the list-rendering path inserts the stored URL into an href without re-validating the scheme, which is inconsistent with the codebase's "URLs validated to http/https" pattern.

  • saveSupplement correctly validates and normalises the URL before persisting it.
  • openSupplementsEditor renders the stored sourceUrl directly into an href without a protocol guard — data arriving via CRDT sync or JSON import bypasses the save-time check.
  • CSS addition in styles.css is clean and consistent with the design system.

Confidence Score: 3/5

The feature works correctly for the normal save flow, but the list renderer trusts stored URLs unconditionally — a crafted import or sync payload can place a javascript: URL into an href.

The URL field and save logic are well-structured, and the CSS changes are safe. The gap is in the rendering step: s.sourceUrl is written into an href without re-checking its scheme, while the rest of the codebase consistently validates URLs to http/https before rendering. Externally-supplied data (JSON import, CRDT sync) skips the save-time guard entirely, so a maliciously crafted entry could produce a live XSS vector in the supplement list.

js/supplements.js — specifically the hostname-extraction block and the href template literal in openSupplementsEditor()

Security Review

  • Unsanitised href in supplement list (js/supplements.js, openSupplementsEditor): s.sourceUrl is inserted into an <a href> using only escapeHTML, with no protocol check at render time. A javascript://hostname.com/ URL would produce a non-empty hostname (passing the sourceHost guard) and be rendered as a clickable link. Exploitation requires injecting a crafted sourceUrl via JSON import or CRDT sync — the normal save path is correctly protected.

Important Files Changed

Filename Overview
js/supplements.js Adds sourceUrl persistence, form visibility, and list-row link rendering. Save path validates http/https correctly, but the render path inserts s.sourceUrl into an href without a scheme re-check, leaving a defense-in-depth gap for externally-injected javascript: URLs.
styles.css Adds two CSS rules for the new .supp-list-source link — color and hover underline. Straightforward and consistent with the design system.

Sequence Diagram

sequenceDiagram
    participant User
    participant Form as Supplement Form
    participant Save as saveSupplement
    participant Store as importedData
    participant List as openSupplementsEditor
    participant Browser

    User->>Form: Paste product URL
    Form->>Save: value of supp-url input
    Save->>Save: validate protocol http or https
    alt Invalid or non-http/https
        Save-->>User: showNotification error
    else Valid
        Save->>Store: "entry.sourceUrl = parsed URL"
    end

    Note over Store: sourceUrl also arrives via CRDT sync or JSON import with no protocol gate

    Store->>List: s.sourceUrl read from data
    List->>List: extract hostname via new URL
    List->>Browser: render href with escapeHTML only, no scheme re-check
Loading

Reviews (1): Last reviewed commit: "Supplements: save product URL for future..." | Re-trigger Greptile

Comment thread js/supplements.js
Comment on lines +465 to +468
let sourceHost = '';
if (s.sourceUrl) {
try { sourceHost = new URL(s.sourceUrl).hostname.replace(/^www\./, ''); } catch { sourceHost = ''; }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security The href uses s.sourceUrl without a scheme check at render time. While saveSupplement validates the protocol, sourceUrl can also arrive via CRDT sync, JSON import, or direct importedData manipulation. A URL of the form javascript://hostname.com/... would have a non-empty hostname (passing the sourceHost truthy check) and be rendered as a clickable XSS-capable link. The fix is to add the same http:/https: guard inside the existing try block that extracts sourceHost, so both variables are derived from the same validated parse.

Suggested change
let sourceHost = '';
if (s.sourceUrl) {
try { sourceHost = new URL(s.sourceUrl).hostname.replace(/^www\./, ''); } catch { sourceHost = ''; }
}
let sourceHost = '';
let safeSourceUrl = '';
if (s.sourceUrl) {
try {
const _parsedUrl = new URL(s.sourceUrl);
if (_parsedUrl.protocol === 'http:' || _parsedUrl.protocol === 'https:') {
sourceHost = _parsedUrl.hostname.replace(/^www\./, '');
safeSourceUrl = s.sourceUrl;
}
} catch { sourceHost = ''; }
}

Comment thread js/supplements.js
<div class="supp-list-info">
<div class="supp-list-name">${escapeHTML(s.name)}${s.dosage ? ` <span class="supp-list-meta">${escapeHTML(s.dosage)}</span>` : ''}</div>
<div class="supp-list-meta">${dateRange}</div>
<div class="supp-list-meta">${dateRange}${sourceHost ? ` &middot; <a href="${escapeHTML(s.sourceUrl)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" class="supp-list-source">${escapeHTML(sourceHost)} ↗</a>` : ''}</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security The href here should use safeSourceUrl (the scheme-validated value) rather than the raw s.sourceUrl. Without this pairing, the scheme guard on sourceHost is bypassed at the href level.

Suggested change
<div class="supp-list-meta">${dateRange}${sourceHost ? ` &middot; <a href="${escapeHTML(s.sourceUrl)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" class="supp-list-source">${escapeHTML(sourceHost)} ↗</a>` : ''}</div>
<div class="supp-list-meta">${dateRange}${sourceHost ? ` &middot; <a href="${escapeHTML(safeSourceUrl)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" class="supp-list-source">${escapeHTML(sourceHost)} ↗</a>` : ''}</div>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant