Skip to content

Commit 76f6a83

Browse files
lazyGPT07lazyGPT07
andauthored
fix(affiliates): reject non-HTTP destination URLs (#723)
* fix(affiliates): reject non-http destination URLs * fix(affiliates): validate remaining deeplink destinations --------- Co-authored-by: lazyGPT07 <288857205+lazyGPT07@users.noreply.github.com>
1 parent 9166ce4 commit 76f6a83

19 files changed

Lines changed: 120 additions & 40 deletions

File tree

packages/affiliates/admitad/src/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ describe('admitad affiliate adapter', () => {
5656
);
5757
});
5858

59+
it('rejects non-HTTP destination URLs before calling Admitad', async () => {
60+
const fetchMock = vi.fn();
61+
vi.stubGlobal('fetch', fetchMock);
62+
63+
await expect(adapter.getTrackingLink?.(
64+
ctx,
65+
'234433',
66+
'javascript:alert(1)',
67+
{ websiteId: '232236' },
68+
)).rejects.toThrow('Admitad destinationUrl must use HTTP or HTTPS');
69+
expect(fetchMock).not.toHaveBeenCalled();
70+
});
71+
5972
it('aggregates publisher website and action statistics', async () => {
6073
vi.stubGlobal('fetch', vi.fn()
6174
.mockResolvedValueOnce(jsonResponse({

packages/affiliates/admitad/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineAffiliate, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
1+
import { defineAffiliate, parseHttpUrl, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
22

33
interface Config {
44
accountId?: string;
@@ -41,6 +41,7 @@ export default defineAffiliate<Config>({
4141
ctx.log(`admitad deeplink · campaign=${programId}`);
4242
const websiteId = admitadWebsiteId(config);
4343
if (!websiteId) throw new Error('Admitad websiteId/accountId is required to generate deeplinks');
44+
if (destinationUrl) parseHttpUrl(destinationUrl, 'Admitad destinationUrl');
4445

4546
const query: Record<string, string | string[]> = { ulp: destinationUrl };
4647
for (const key of ['subid', 'subid1', 'subid2', 'subid3', 'subid4'] as const) {

packages/affiliates/cj/src/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ describe('CJ affiliate adapter', () => {
5858
expect(request.headers.authorization).toBe('Bearer cj-token');
5959
});
6060

61+
it('rejects non-HTTP destination URLs before calling Link Search', async () => {
62+
const fetchMock = vi.fn();
63+
vi.stubGlobal('fetch', fetchMock);
64+
65+
await expect(adapter.getTrackingLink?.(
66+
ctx(),
67+
'15058',
68+
'data:text/html,hello',
69+
{ websiteId: '12345' },
70+
)).rejects.toThrow('CJ destinationUrl must use HTTP or HTTPS');
71+
expect(fetchMock).not.toHaveBeenCalled();
72+
});
73+
6174
it('falls back to the first HTML href when Link Search omits clickUrl', async () => {
6275
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(xmlResponse(`
6376
<cj-api>

packages/affiliates/cj/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineAffiliate, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
1+
import { defineAffiliate, parseHttpUrl, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
22

33
interface Config {
44
accountId?: string;
@@ -33,6 +33,7 @@ export default defineAffiliate<Config>({
3333
ctx.log(`cj link search · advertiser=${programId}`);
3434
const websiteId = config.websiteId;
3535
if (!websiteId) throw new Error('CJ websiteId is required to generate publisher tracking links');
36+
if (destinationUrl) parseHttpUrl(destinationUrl, 'CJ destinationUrl');
3637

3738
const xml = await cjLinkSearch(ctx, config, {
3839
'website-id': websiteId,

packages/affiliates/ebay-partner/src/index.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ describe('eBay Partner Network affiliate adapter', () => {
7878
);
7979
});
8080

81+
it('rejects non-HTTP destination URLs', async () => {
82+
await expect(adapter.getTrackingLink?.(
83+
ctx(),
84+
'5338461150',
85+
'javascript:alert(1)',
86+
{},
87+
)).rejects.toThrow('destinationUrl must use HTTP or HTTPS');
88+
});
89+
8190
it('aggregates Partner Reporting campaign metrics', async () => {
8291
const fetchMock = vi.fn().mockResolvedValue(jsonTextResponse({
8392
Records: [

packages/affiliates/ebay-partner/src/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineAffiliate, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
1+
import { defineAffiliate, parseHttpUrl, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
22

33
interface Config {
44
accountId?: string;
@@ -48,12 +48,7 @@ export default defineAffiliate<Config>({
4848
if (!campaignId) throw new Error('eBay Partner campaignId is required to build tracking links');
4949
if (!destinationUrl) throw new Error('eBay Partner destinationUrl is required to build tracking links');
5050

51-
let url: URL;
52-
try {
53-
url = new URL(destinationUrl);
54-
} catch {
55-
throw new Error('eBay Partner destinationUrl must be an absolute URL');
56-
}
51+
const url = parseHttpUrl(destinationUrl, 'eBay Partner destinationUrl');
5752

5853
url.searchParams.set('mkevt', config.eventType ?? DEFAULT_EVENT_TYPE);
5954
url.searchParams.set('mkcid', config.channelId ?? DEFAULT_CHANNEL_ID);

packages/affiliates/flexoffers/src/index.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ describe('FlexOffers affiliate adapter', () => {
100100
)).rejects.toThrow('Domain ID is required');
101101
});
102102

103+
it('rejects non-HTTP destination URLs', async () => {
104+
await expect(adapter.getTrackingLink?.(
105+
ctx({}),
106+
'171465',
107+
'ftp://merchant.example/product',
108+
{ accountId: '177', useDeeplinkApi: false },
109+
)).rejects.toThrow('destinationUrl must use HTTP or HTTPS');
110+
});
111+
103112
it('loads sales stats across explicit statuses', async () => {
104113
const fetchMock = vi.fn()
105114
.mockResolvedValueOnce(jsonResponse({

packages/affiliates/flexoffers/src/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineAffiliate, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
1+
import { defineAffiliate, parseHttpUrl, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
22

33
interface Config {
44
accountId?: string;
@@ -57,11 +57,7 @@ export default defineAffiliate<Config>({
5757
async getTrackingLink(ctx, programId, destinationUrl, config) {
5858
ctx.log(`flexoffers deeplink · advertiser=${programId}`);
5959
if (!destinationUrl) throw new Error('FlexOffers destinationUrl is required');
60-
try {
61-
new URL(destinationUrl);
62-
} catch {
63-
throw new Error('FlexOffers destinationUrl must be an absolute URL');
64-
}
60+
parseHttpUrl(destinationUrl, 'FlexOffers destinationUrl');
6561

6662
if (config.useDeeplinkApi !== false) {
6763
const data = await flexoffersGet(ctx, config, '/deeplink', deeplinkQuery(programId, destinationUrl, config));

packages/affiliates/impact/src/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ describe('Impact affiliate adapter', () => {
6565
expect(request.method).toBe('POST');
6666
});
6767

68+
it('rejects non-HTTP destination URLs before calling Impact', async () => {
69+
const fetchMock = vi.fn();
70+
vi.stubGlobal('fetch', fetchMock);
71+
72+
await expect(adapter.getTrackingLink?.(
73+
ctx(),
74+
'10000',
75+
'mailto:merchant@example.com',
76+
{ accountId: 'IRSid' },
77+
)).rejects.toThrow('Impact destinationUrl must use HTTP or HTTPS');
78+
expect(fetchMock).not.toHaveBeenCalled();
79+
});
80+
6881
it('supports regular program-level links when no deeplink is provided', async () => {
6982
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonTextResponse({
7083
TrackingUrl: 'https://example.sjv.io/c/123456/98765/101010',

packages/affiliates/impact/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineAffiliate, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
1+
import { defineAffiliate, parseHttpUrl, tokenSetup, type AffiliateConnectContext } from '@profullstack/sh1pt-core';
22

33
interface Config {
44
accountId?: string;
@@ -36,6 +36,7 @@ export default defineAffiliate<Config>({
3636
async getTrackingLink(ctx, programId, destinationUrl, config) {
3737
ctx.log(`impact tracking link - program=${programId}`);
3838
const { accountSid } = requireAuth(ctx, config);
39+
if (destinationUrl) parseHttpUrl(destinationUrl, 'Impact destinationUrl');
3940
const query = trackingLinkQuery(destinationUrl, config);
4041
const data = await impactRequest(
4142
ctx,

0 commit comments

Comments
 (0)