Skip to content

fix(affiliates): reject non-HTTP destination URLs#723

Merged
ralyodio merged 2 commits into
profullstack:masterfrom
lazyGPT07:codex/reject-non-http-affiliate-links
Jun 14, 2026
Merged

fix(affiliates): reject non-HTTP destination URLs#723
ralyodio merged 2 commits into
profullstack:masterfrom
lazyGPT07:codex/reject-non-http-affiliate-links

Conversation

@lazyGPT07

Copy link
Copy Markdown
Contributor

Summary

  • add a shared parseHttpUrl helper that accepts only absolute HTTP/HTTPS URLs
  • apply it to eBay Partner, FlexOffers, Rakuten, ShareASale, Sovrn, and Tradedoubler
  • add one non-HTTP destination regression test per affected adapter

Why

new URL() accepts schemes such as javascript:, data:, mailto:, and ftp:. The affected adapters previously treated those values as valid destinations; eBay could emit a direct non-web URL, while redirect-style adapters could encode one into a tracking link.

Fixes #722.

Validation

  • focused affiliate tests: 69 passed across 6 files
  • affected packages: 7 typechecks passed
  • affected packages: 7 builds passed
  • full test run: 2,701 passed, 1 skipped
  • full run also reported 12 unrelated clean-checkout package-resolution failures and one pre-existing Windows path-separator assertion in the VS Code target tests
  • git diff --check passed

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a shared parseHttpUrl helper in packages/core/src/affiliate.ts that rejects any URL whose scheme is not http: or https:, and applies it to nine affiliate adapters (Admitad, CJ, eBay Partner, FlexOffers, Impact, Rakuten, ShareASale, Sovrn, Tradedoubler) that previously accepted javascript:, data:, ftp:, and other non-web schemes as valid destination URLs.

  • New helper: parseHttpUrl(value, label) — wraps new URL() and adds an explicit protocol check, exported from the core package so every adapter can share it.
  • Nine adapters updated: redirect-style adapters (Rakuten, ShareASale, Sovrn, Tradedoubler, FlexOffers) replace their old try/new URL() blocks outright; API-call adapters (Admitad, CJ, Impact) use an if (destinationUrl) conditional guard since their destination is optional; eBay Partner uses the returned URL object directly.
  • Regression tests: one non-HTTP test case added per adapter, with fetch-mock assertions confirming the network is never called when the URL is invalid.

Confidence Score: 4/5

The core fix is correct and well-tested, but two adapters outside the PR's explicit scope — awin and digistore24 — still pass unvalidated destinationUrl values to external APIs using the same redirect-encoding pattern the PR is designed to eliminate.

The nine adapters touched in this PR are correctly hardened, and the shared parseHttpUrl helper is a clean abstraction. However, a survey of the full adapter set reveals that packages/affiliates/awin/src/index.ts (line 46, body.destinationUrl = destinationUrl) and packages/affiliates/digistore24/src/index.ts (line 126, fallback_url: destinationUrl) forward the caller-supplied URL to external APIs without a protocol check — the same gap that triggered this fix.

packages/affiliates/awin/src/index.ts and packages/affiliates/digistore24/src/index.ts each encode destinationUrl into an outbound API request without the HTTP protocol guard added in this PR.

Important Files Changed

Filename Overview
packages/core/src/affiliate.ts Adds parseHttpUrl helper that validates HTTP/HTTPS protocol before parsing; exported from the package so all adapters can use it.
packages/affiliates/ebay-partner/src/index.ts Replaces inline try/new URL() with parseHttpUrl; returned URL object is used directly for appending query params — clean refactor.
packages/affiliates/rakuten/src/index.ts Replaces inline try/new URL() validation-only call with parseHttpUrl; correctness maintained, now also rejects non-HTTP schemes.
packages/affiliates/shareasale/src/index.ts Replaces inline try/new URL() with parseHttpUrl; destinationUrl is required before this call so no conditional guard needed.
packages/affiliates/sovrn/src/index.ts Same pattern as ShareASale/Rakuten; destinationUrl is required before the parseHttpUrl call; the return value is intentionally discarded.
packages/affiliates/tradedoubler/src/index.ts Same pattern as ShareASale/Rakuten; correctly guarded by required-destination check before parseHttpUrl.
packages/affiliates/flexoffers/src/index.ts Replaces inline try/catch URL validation with parseHttpUrl; covers both the deeplink-API path and the non-deeplink redirect path.
packages/affiliates/admitad/src/index.ts Uses if (destinationUrl) parseHttpUrl(...) guard because destination is optional; truthy non-HTTP values are correctly rejected while empty/absent values are intentionally passed through.
packages/affiliates/cj/src/index.ts Same optional-destination guard pattern as Admitad; non-HTTP values rejected before the CJ Link Search API call.
packages/affiliates/impact/src/index.ts Same optional-destination guard pattern; non-HTTP values rejected before the Impact API call.
packages/affiliates/ebay-partner/src/index.test.ts Adds regression test for javascript: scheme rejection; covers the case where eBay Partner previously modified the non-HTTP URL in-place.
packages/affiliates/flexoffers/src/index.test.ts Adds regression test using ftp:// scheme (a non-HTTP URL that is still valid per new URL()); tests the non-deeplink path.
packages/affiliates/admitad/src/index.test.ts Regression test verifies javascript: is rejected AND that the Admitad API is never called when the URL is invalid.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getTrackingLink called\nwith destinationUrl] --> B{destinationUrl\nprovided?}
    B -- No --> C[Skip validation\npass empty/null to API]
    B -- Yes --> D[parseHttpUrl]
    D --> E{new URL\nparses?}
    E -- No --> F[throw: must be\nan absolute URL]
    E -- Yes --> G{protocol is\nhttp: or https:?}
    G -- No --> H[throw: must use\nHTTP or HTTPS]
    G -- Yes --> I[Return URL object]
    I --> J[Adapter builds\ntracking link]

    subgraph "Not yet guarded"
        K[awin: body.destinationUrl = destinationUrl]
        L[digistore24: fallback_url: destinationUrl]
    end
Loading

Reviews (3): Last reviewed commit: "fix(affiliates): validate remaining deep..." | Re-trigger Greptile

Comment on lines +79 to +90
export function parseHttpUrl(value: string, label = 'URL'): URL {
let url: URL;
try {
url = new URL(value);
} catch {
throw new Error(`${label} must be an absolute URL`);
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error(`${label} must use HTTP or HTTPS`);
}
return url;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Three redirect-style adapters still encode non-HTTP destination URLs

parseHttpUrl is correctly applied to 9 adapters in this PR, but skimlinks, everflow, and refersion share the exact same redirect-encoding pattern and were not updated:

  • packages/affiliates/skimlinks/src/index.ts line 44: url.searchParams.set('url', destinationUrl)destinationUrl is set directly as the redirect target url query parameter with no protocol check (same pattern as the now-fixed Rakuten/Sovrn/Tradedoubler/ShareASale adapters).
  • packages/affiliates/everflow/src/index.ts withFallbackDestination: parsed.searchParams.set('url', destinationUrl) — same issue.
  • packages/affiliates/refersion/src/index.ts withDestinationSubId: url.searchParams.set('u', destinationUrl)destinationUrl is encoded as a sub-parameter on the referral link with no validation.

A javascript:alert(1) or data: URL passed to any of those three adapters today will be embedded in the returned tracking link, bypassing the fix introduced here.

@lazyGPT07

Copy link
Copy Markdown
Contributor Author

Addressed the remaining adapter coverage in 1b42e1e: Admitad, CJ, and Impact now validate non-empty deeplink destinations through parseHttpUrl, with one fail-before-fetch regression per adapter.

Validation: all nine affected affiliate suites pass (101 tests); Admitad, CJ, and Impact typechecks and builds pass; git diff --check passes.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@lazyGPT07 lazyGPT07 force-pushed the codex/reject-non-http-affiliate-links branch from 1b42e1e to d8b2aba Compare June 14, 2026 03:05
@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

13 similar comments
@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@github-actions

Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@ralyodio ralyodio merged commit 76f6a83 into profullstack:master Jun 14, 2026
5 checks passed
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.

Affiliate link adapters accept non-HTTP destination URLs

2 participants