From 8a090a57d4224c00ea506b4ebb150574434d95b3 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:50:23 -0700 Subject: [PATCH 1/5] add url sanitization --- .../src/orders/dtos/tracking-cost.dto.ts | 7 +++- apps/backend/src/orders/order.service.spec.ts | 23 +++++++++--- apps/backend/src/orders/order.service.ts | 12 ++++++- .../src/utils/validation.utils.spec.ts | 36 ++++++++++++++++++- apps/backend/src/utils/validation.utils.ts | 21 +++++++++++ 5 files changed, 92 insertions(+), 7 deletions(-) diff --git a/apps/backend/src/orders/dtos/tracking-cost.dto.ts b/apps/backend/src/orders/dtos/tracking-cost.dto.ts index 1c29ce6eb..273273842 100644 --- a/apps/backend/src/orders/dtos/tracking-cost.dto.ts +++ b/apps/backend/src/orders/dtos/tracking-cost.dto.ts @@ -1,7 +1,12 @@ import { IsUrl, IsNumber, Min, IsOptional } from 'class-validator'; export class TrackingCostDto { - @IsUrl({}, { message: 'Tracking link must be a valid URL' }) + @IsUrl( + { + protocols: ['http', 'https'], + }, + { message: 'Tracking link must be a valid HTTP/HTTPS URL' }, + ) @IsOptional() trackingLink?: string; diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 20510d45a..d01b63195 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -365,7 +365,7 @@ describe('OrdersService', () => { ); }); - it('updates tracking link for shipped order', async () => { + it('sanitizes and updates tracking link for shipped order', async () => { const trackingCostDto: TrackingCostDto = { trackingLink: 'samplelink.com', }; @@ -374,7 +374,7 @@ describe('OrdersService', () => { const order = await service.findOne(3); expect(order.trackingLink).toBeDefined(); - expect(order.trackingLink).toEqual('samplelink.com'); + expect(order.trackingLink).toEqual('https://samplelink.com/'); }); it('updates shipping cost for shipped order', async () => { @@ -389,7 +389,7 @@ describe('OrdersService', () => { expect(order.shippingCost).toEqual('12.99'); }); - it('updates both shipping cost and tracking link', async () => { + it('updates both shipping cost and tracking link (sanitized)', async () => { const trackingCostDto: TrackingCostDto = { trackingLink: 'testtracking.com', shippingCost: 7.5, @@ -398,7 +398,7 @@ describe('OrdersService', () => { await service.updateTrackingCostInfo(3, trackingCostDto); const order = await service.findOne(3); - expect(order.trackingLink).toEqual('testtracking.com'); + expect(order.trackingLink).toEqual('https://testtracking.com/'); expect(order.shippingCost).toEqual('7.50'); }); @@ -442,6 +442,21 @@ describe('OrdersService', () => { ); }); + it('throws when tracking link is invalid', async () => { + const trackingCostDto: TrackingCostDto = { + trackingLink: `javascript:alert("you've been hacked!")`, + shippingCost: 7.5, + }; + + await expect( + service.updateTrackingCostInfo(3, trackingCostDto), + ).rejects.toThrow( + new BadRequestException( + 'Invalid tracking link. Only valid HTTP/HTTPS URLs are accepted.', + ), + ); + }); + it('sets status to shipped when both fields provided and previous status pending', async () => { const trackingCostDto: TrackingCostDto = { trackingLink: 'testtracking.com', diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 601b7ba42..4f8203038 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -8,7 +8,7 @@ import { Repository, In } from 'typeorm'; import { Order } from './order.entity'; import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; -import { validateId } from '../utils/validation.utils'; +import { sanitizeUrl, validateId } from '../utils/validation.utils'; import { OrderStatus } from './types'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; import { OrderDetailsDto } from './dtos/order-details.dto'; @@ -283,6 +283,16 @@ export class OrdersService { ); } + if (dto.trackingLink) { + const sanitized = sanitizeUrl(dto.trackingLink); + if (!sanitized) { + throw new BadRequestException( + 'Invalid tracking link. Only valid HTTP/HTTPS URLs are accepted.', + ); + } + dto.trackingLink = sanitized; + } + const order = await this.repo.findOneBy({ orderId }); if (!order) { throw new NotFoundException(`Order ${orderId} not found`); diff --git a/apps/backend/src/utils/validation.utils.spec.ts b/apps/backend/src/utils/validation.utils.spec.ts index 01c1f7901..5d59013e5 100644 --- a/apps/backend/src/utils/validation.utils.spec.ts +++ b/apps/backend/src/utils/validation.utils.spec.ts @@ -2,7 +2,7 @@ import { BadRequestException, InternalServerErrorException, } from '@nestjs/common'; -import { validateEnv, validateId } from './validation.utils'; +import { sanitizeUrl, validateEnv, validateId } from './validation.utils'; describe('validateId', () => { it('should not throw an error for a valid ID', () => { @@ -39,3 +39,37 @@ describe('validateEnv', () => { ); }); }); + +describe('sanitizeUrl', () => { + it('should return null for malicious protocols', () => { + const maliciousProtocols = ['javascript:', 'data:', 'file:', 'vbscript:']; + + for (const protocol of maliciousProtocols) { + expect(sanitizeUrl(protocol + 'test')).toBeNull(); + } + }); + + it('should return null for empty URLs', () => { + expect(sanitizeUrl('')).toBeNull(); + expect(sanitizeUrl('https://')).toBeNull(); + }); + + it('should accept valid http/https URLs', () => { + const validHttpUrl = 'http://www.tracking.com/test'; + const validHttpsUrl = 'https://www.tracking.com/test'; + expect(sanitizeUrl(validHttpUrl)).toBe(validHttpUrl); + expect(sanitizeUrl(validHttpsUrl)).toBe(validHttpsUrl); + }); + + it('adds https:// to URL without protocol', () => { + expect(sanitizeUrl('www.tracking.com/test')).toBe( + 'https://www.tracking.com/test', + ); + }); + + it('trims whitespace from URL', () => { + expect(sanitizeUrl(' https://www.tracking.com/test ')).toBe( + 'https://www.tracking.com/test', + ); + }); +}); diff --git a/apps/backend/src/utils/validation.utils.ts b/apps/backend/src/utils/validation.utils.ts index be7f141a3..bad459684 100644 --- a/apps/backend/src/utils/validation.utils.ts +++ b/apps/backend/src/utils/validation.utils.ts @@ -18,3 +18,24 @@ export function validateEnv(name: string): string { return v; } + +export function sanitizeUrl(url: string): string | null { + try { + const trimmed = url.trim(); + if (!trimmed) return null; + + let fullUrl = trimmed; + if (!/^https?:\/\//i.test(trimmed)) { + fullUrl = 'https://' + trimmed; + } + + const urlObj = new URL(fullUrl); + + if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') + return null; + if (!urlObj.hostname || urlObj.hostname.length === 0) return null; + return urlObj.href; + } catch { + return null; + } +} From 4bc9ebdb82dbfc7c39fa00d51b462e1bc721cdf2 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:41:22 -0700 Subject: [PATCH 2/5] review comments --- apps/backend/src/config/migrations.ts | 2 ++ .../1773041840374-FixTrackingLinks.ts | 19 +++++++++++++++++++ .../src/utils/validation.utils.spec.ts | 3 ++- apps/backend/src/utils/validation.utils.ts | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/migrations/1773041840374-FixTrackingLinks.ts diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 85684cd70..2d8e56402 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -33,6 +33,7 @@ import { UpdateOrderEntity1769990652833 } from '../migrations/1769990652833-Upda import { DonationItemFoodTypeNotNull1771524930613 } from '../migrations/1771524930613-DonationItemFoodTypeNotNull'; import { MoveRequestFieldsToOrders1770571145350 } from '../migrations/1770571145350-MoveRequestFieldsToOrders'; import { RenameDonationMatchingStatus1771260403657 } from '../migrations/1771260403657-RenameDonationMatchingStatus'; +import { FixTrackingLinks1773041840374 } from '../migrations/1773041840374-FixTrackingLinks'; const schemaMigrations = [ User1725726359198, @@ -70,6 +71,7 @@ const schemaMigrations = [ DonationItemFoodTypeNotNull1771524930613, MoveRequestFieldsToOrders1770571145350, RenameDonationMatchingStatus1771260403657, + FixTrackingLinks1773041840374, ]; export default schemaMigrations; diff --git a/apps/backend/src/migrations/1773041840374-FixTrackingLinks.ts b/apps/backend/src/migrations/1773041840374-FixTrackingLinks.ts new file mode 100644 index 000000000..8449da3e5 --- /dev/null +++ b/apps/backend/src/migrations/1773041840374-FixTrackingLinks.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FixTrackingLinks1773041840374 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE orders + SET tracking_link = 'https://www.samplelink.com/samplelink' + WHERE tracking_link = 'www.samplelink/samplelink'; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE orders + SET tracking_link = 'www.samplelink/samplelink' + WHERE tracking_link = 'https://www.samplelink.com/samplelink'; + `); + } +} diff --git a/apps/backend/src/utils/validation.utils.spec.ts b/apps/backend/src/utils/validation.utils.spec.ts index 5d59013e5..2d2905d03 100644 --- a/apps/backend/src/utils/validation.utils.spec.ts +++ b/apps/backend/src/utils/validation.utils.spec.ts @@ -49,9 +49,10 @@ describe('sanitizeUrl', () => { } }); - it('should return null for empty URLs', () => { + it('should return null for empty or invalid URLs', () => { expect(sanitizeUrl('')).toBeNull(); expect(sanitizeUrl('https://')).toBeNull(); + expect(sanitizeUrl('https://foo')).toBeNull(); }); it('should accept valid http/https URLs', () => { diff --git a/apps/backend/src/utils/validation.utils.ts b/apps/backend/src/utils/validation.utils.ts index bad459684..700f757e6 100644 --- a/apps/backend/src/utils/validation.utils.ts +++ b/apps/backend/src/utils/validation.utils.ts @@ -33,7 +33,7 @@ export function sanitizeUrl(url: string): string | null { if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') return null; - if (!urlObj.hostname || urlObj.hostname.length === 0) return null; + if (!urlObj.hostname || !urlObj.hostname.includes('.')) return null; return urlObj.href; } catch { return null; From 06ce9897a570da77c9ccd35a8cafd27bf5e6b29a Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:53:43 -0700 Subject: [PATCH 3/5] fix tests --- apps/backend/src/foodRequests/request.service.spec.ts | 2 +- apps/backend/src/orders/order.service.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index d404d62a0..99432e504 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -120,7 +120,7 @@ describe('RequestsService', () => { orderId: 1, status: OrderStatus.DELIVERED, foodManufacturerName: 'FoodCorp Industries', - trackingLink: 'www.samplelink/samplelink', + trackingLink: 'https://www.samplelink.com/samplelink', items: expectedItems, }); }); diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 94d1f5bf1..78dc5a7be 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -156,7 +156,7 @@ describe('OrdersService', () => { orderId: 1, status: OrderStatus.DELIVERED, foodManufacturerName: 'FoodCorp Industries', - trackingLink: 'www.samplelink/samplelink', + trackingLink: 'https://www.samplelink.com/samplelink', items: [ { id: 1, @@ -358,7 +358,7 @@ describe('OrdersService', () => { describe('updateTrackingCostInfo', () => { it('throws when order is non-existent', async () => { const trackingCostDto: TrackingCostDto = { - trackingLink: 'test', + trackingLink: 'www.test.com', shippingCost: 5.99, }; From 72f1e6a79194781442d55c762def227b244f9d4c Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:42:47 -0400 Subject: [PATCH 4/5] add dns lookup --- apps/backend/src/orders/order.service.ts | 2 +- .../src/utils/validation.utils.spec.ts | 47 +++++++++++++------ apps/backend/src/utils/validation.utils.ts | 6 ++- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 8c381499c..db021a7c2 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -284,7 +284,7 @@ export class OrdersService { } if (dto.trackingLink) { - const sanitized = sanitizeUrl(dto.trackingLink); + const sanitized = await sanitizeUrl(dto.trackingLink); if (!sanitized) { throw new BadRequestException( 'Invalid tracking link. Only valid HTTP/HTTPS URLs are accepted.', diff --git a/apps/backend/src/utils/validation.utils.spec.ts b/apps/backend/src/utils/validation.utils.spec.ts index 2d2905d03..83ec4c30c 100644 --- a/apps/backend/src/utils/validation.utils.spec.ts +++ b/apps/backend/src/utils/validation.utils.spec.ts @@ -3,6 +3,10 @@ import { InternalServerErrorException, } from '@nestjs/common'; import { sanitizeUrl, validateEnv, validateId } from './validation.utils'; +import { promises as dns } from 'dns'; + +jest.mock('dns', () => ({ promises: { lookup: jest.fn() } })); +const mockLookup = dns.lookup as jest.Mock; describe('validateId', () => { it('should not throw an error for a valid ID', () => { @@ -40,37 +44,52 @@ describe('validateEnv', () => { }); }); -describe('sanitizeUrl', () => { - it('should return null for malicious protocols', () => { +describe('await sanitizeUrl', () => { + it('should return null for malicious protocols', async () => { const maliciousProtocols = ['javascript:', 'data:', 'file:', 'vbscript:']; for (const protocol of maliciousProtocols) { - expect(sanitizeUrl(protocol + 'test')).toBeNull(); + expect(await sanitizeUrl(protocol + 'test')).toBeNull(); } }); - it('should return null for empty or invalid URLs', () => { - expect(sanitizeUrl('')).toBeNull(); - expect(sanitizeUrl('https://')).toBeNull(); - expect(sanitizeUrl('https://foo')).toBeNull(); + it('should return null for empty or invalid URLs', async () => { + expect(await sanitizeUrl('')).toBeNull(); + expect(await sanitizeUrl('https://')).toBeNull(); + expect(await sanitizeUrl('https://foo')).toBeNull(); }); - it('should accept valid http/https URLs', () => { + it('should accept valid http/https URLs', async () => { + mockLookup.mockResolvedValue({ address: '127.0.0.1', family: 4 }); + const validHttpUrl = 'http://www.tracking.com/test'; const validHttpsUrl = 'https://www.tracking.com/test'; - expect(sanitizeUrl(validHttpUrl)).toBe(validHttpUrl); - expect(sanitizeUrl(validHttpsUrl)).toBe(validHttpsUrl); + expect(await sanitizeUrl(validHttpUrl)).toBe(validHttpUrl); + expect(await sanitizeUrl(validHttpsUrl)).toBe(validHttpsUrl); }); - it('adds https:// to URL without protocol', () => { - expect(sanitizeUrl('www.tracking.com/test')).toBe( + it('adds https:// to URL without protocol', async () => { + mockLookup.mockResolvedValue({ address: '127.0.0.1', family: 4 }); + + expect(await sanitizeUrl('www.tracking.com/test')).toBe( 'https://www.tracking.com/test', ); }); - it('trims whitespace from URL', () => { - expect(sanitizeUrl(' https://www.tracking.com/test ')).toBe( + it('trims whitespace from URL', async () => { + mockLookup.mockResolvedValue({ address: '127.0.0.1', family: 4 }); + + expect(await sanitizeUrl(' https://www.tracking.com/test ')).toBe( 'https://www.tracking.com/test', ); }); + + it('returns null for unreachable hostname', async () => { + mockLookup.mockRejectedValueOnce(new Error('DNS lookup failed')); + + const result = await sanitizeUrl('https://www.fakefakefake.com'); + + expect(result).toBeNull(); + expect(mockLookup).toHaveBeenCalledWith('www.fakefakefake.com'); + }); }); diff --git a/apps/backend/src/utils/validation.utils.ts b/apps/backend/src/utils/validation.utils.ts index 700f757e6..6ced30d53 100644 --- a/apps/backend/src/utils/validation.utils.ts +++ b/apps/backend/src/utils/validation.utils.ts @@ -2,6 +2,7 @@ import { BadRequestException, InternalServerErrorException, } from '@nestjs/common'; +import { promises as dns } from 'dns'; export function validateId(id: number, entityName: string): void { if (!id || id < 1) { @@ -19,7 +20,7 @@ export function validateEnv(name: string): string { return v; } -export function sanitizeUrl(url: string): string | null { +export async function sanitizeUrl(url: string): Promise { try { const trimmed = url.trim(); if (!trimmed) return null; @@ -34,6 +35,9 @@ export function sanitizeUrl(url: string): string | null { if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') return null; if (!urlObj.hostname || !urlObj.hostname.includes('.')) return null; + + await dns.lookup(urlObj.hostname); + return urlObj.href; } catch { return null; From 8bb008a31192d50aaa4e21e486b8b9ecac1b1fcb Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:43:48 -0400 Subject: [PATCH 5/5] Revert "add dns lookup" This reverts commit 72f1e6a79194781442d55c762def227b244f9d4c. --- apps/backend/src/orders/order.service.ts | 2 +- .../src/utils/validation.utils.spec.ts | 47 ++++++------------- apps/backend/src/utils/validation.utils.ts | 6 +-- 3 files changed, 16 insertions(+), 39 deletions(-) diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index db021a7c2..8c381499c 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -284,7 +284,7 @@ export class OrdersService { } if (dto.trackingLink) { - const sanitized = await sanitizeUrl(dto.trackingLink); + const sanitized = sanitizeUrl(dto.trackingLink); if (!sanitized) { throw new BadRequestException( 'Invalid tracking link. Only valid HTTP/HTTPS URLs are accepted.', diff --git a/apps/backend/src/utils/validation.utils.spec.ts b/apps/backend/src/utils/validation.utils.spec.ts index 83ec4c30c..2d2905d03 100644 --- a/apps/backend/src/utils/validation.utils.spec.ts +++ b/apps/backend/src/utils/validation.utils.spec.ts @@ -3,10 +3,6 @@ import { InternalServerErrorException, } from '@nestjs/common'; import { sanitizeUrl, validateEnv, validateId } from './validation.utils'; -import { promises as dns } from 'dns'; - -jest.mock('dns', () => ({ promises: { lookup: jest.fn() } })); -const mockLookup = dns.lookup as jest.Mock; describe('validateId', () => { it('should not throw an error for a valid ID', () => { @@ -44,52 +40,37 @@ describe('validateEnv', () => { }); }); -describe('await sanitizeUrl', () => { - it('should return null for malicious protocols', async () => { +describe('sanitizeUrl', () => { + it('should return null for malicious protocols', () => { const maliciousProtocols = ['javascript:', 'data:', 'file:', 'vbscript:']; for (const protocol of maliciousProtocols) { - expect(await sanitizeUrl(protocol + 'test')).toBeNull(); + expect(sanitizeUrl(protocol + 'test')).toBeNull(); } }); - it('should return null for empty or invalid URLs', async () => { - expect(await sanitizeUrl('')).toBeNull(); - expect(await sanitizeUrl('https://')).toBeNull(); - expect(await sanitizeUrl('https://foo')).toBeNull(); + it('should return null for empty or invalid URLs', () => { + expect(sanitizeUrl('')).toBeNull(); + expect(sanitizeUrl('https://')).toBeNull(); + expect(sanitizeUrl('https://foo')).toBeNull(); }); - it('should accept valid http/https URLs', async () => { - mockLookup.mockResolvedValue({ address: '127.0.0.1', family: 4 }); - + it('should accept valid http/https URLs', () => { const validHttpUrl = 'http://www.tracking.com/test'; const validHttpsUrl = 'https://www.tracking.com/test'; - expect(await sanitizeUrl(validHttpUrl)).toBe(validHttpUrl); - expect(await sanitizeUrl(validHttpsUrl)).toBe(validHttpsUrl); + expect(sanitizeUrl(validHttpUrl)).toBe(validHttpUrl); + expect(sanitizeUrl(validHttpsUrl)).toBe(validHttpsUrl); }); - it('adds https:// to URL without protocol', async () => { - mockLookup.mockResolvedValue({ address: '127.0.0.1', family: 4 }); - - expect(await sanitizeUrl('www.tracking.com/test')).toBe( + it('adds https:// to URL without protocol', () => { + expect(sanitizeUrl('www.tracking.com/test')).toBe( 'https://www.tracking.com/test', ); }); - it('trims whitespace from URL', async () => { - mockLookup.mockResolvedValue({ address: '127.0.0.1', family: 4 }); - - expect(await sanitizeUrl(' https://www.tracking.com/test ')).toBe( + it('trims whitespace from URL', () => { + expect(sanitizeUrl(' https://www.tracking.com/test ')).toBe( 'https://www.tracking.com/test', ); }); - - it('returns null for unreachable hostname', async () => { - mockLookup.mockRejectedValueOnce(new Error('DNS lookup failed')); - - const result = await sanitizeUrl('https://www.fakefakefake.com'); - - expect(result).toBeNull(); - expect(mockLookup).toHaveBeenCalledWith('www.fakefakefake.com'); - }); }); diff --git a/apps/backend/src/utils/validation.utils.ts b/apps/backend/src/utils/validation.utils.ts index 6ced30d53..700f757e6 100644 --- a/apps/backend/src/utils/validation.utils.ts +++ b/apps/backend/src/utils/validation.utils.ts @@ -2,7 +2,6 @@ import { BadRequestException, InternalServerErrorException, } from '@nestjs/common'; -import { promises as dns } from 'dns'; export function validateId(id: number, entityName: string): void { if (!id || id < 1) { @@ -20,7 +19,7 @@ export function validateEnv(name: string): string { return v; } -export async function sanitizeUrl(url: string): Promise { +export function sanitizeUrl(url: string): string | null { try { const trimmed = url.trim(); if (!trimmed) return null; @@ -35,9 +34,6 @@ export async function sanitizeUrl(url: string): Promise { if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') return null; if (!urlObj.hostname || !urlObj.hostname.includes('.')) return null; - - await dns.lookup(urlObj.hostname); - return urlObj.href; } catch { return null;