From 1c876464313d4b4e217f1704af73dcac038b5764 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 1 Oct 2026 16:49:18 +0000 Subject: [PATCH 1/3] fix: reject array address query parameters Next.js turns a repeated query key into an array. parseAddress called string methods on that value, so includes() checked element membership instead of a substring. Non-string addresses are now rejected. Co-authored-by: David --- tests/unittests/validators.test.ts | 6 ++++++ utils/validators.ts | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/unittests/validators.test.ts b/tests/unittests/validators.test.ts index 6bbf8dd2e..58c697ed5 100644 --- a/tests/unittests/validators.test.ts +++ b/tests/unittests/validators.test.ts @@ -109,6 +109,12 @@ describe('parseAddress', () => { v.parseAddress('') }).toThrow(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message) }) + + it('Reject repeated query parameters passed as an array', () => { + expect(() => { + v.parseAddress([exampleAddresses.ecash, exampleAddresses.bitcoincash] as unknown as string) + }).toThrow(RESPONSE_MESSAGES.INVALID_ADDRESS_400.message) + }) }) describe('parseError', () => { diff --git a/utils/validators.ts b/utils/validators.ts index 6f4c2eaa6..f8b809817 100644 --- a/utils/validators.ts +++ b/utils/validators.ts @@ -28,7 +28,10 @@ const parseAddressTextBlock = function (addressBlock: string): string[] { .filter((value, index, self) => self.indexOf(value) === index) } -export const parseAddress = function (addressString: string | undefined): string { +export const parseAddress = function (addressString: string | string[] | undefined): string { + // Repeated query keys arrive as arrays. String methods like includes() then + // check membership instead of a substring, so reject non-strings before use. + if (Array.isArray(addressString)) throw new Error(RESPONSE_MESSAGES.INVALID_ADDRESS_400.message) if (addressString === '' || addressString === undefined) throw new Error(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message) let parsedAddress: string if ( From bfa17be2f20fc78ee93c03d37090a3f2e5644b36 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 1 Oct 2026 10:56:01 -0700 Subject: [PATCH 2/3] Potential fix for pull request finding 'Avoid casting array input to preserve parseAddress type coverage' Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/unittests/validators.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/validators.test.ts b/tests/unittests/validators.test.ts index 58c697ed5..934400eeb 100644 --- a/tests/unittests/validators.test.ts +++ b/tests/unittests/validators.test.ts @@ -112,7 +112,7 @@ describe('parseAddress', () => { it('Reject repeated query parameters passed as an array', () => { expect(() => { - v.parseAddress([exampleAddresses.ecash, exampleAddresses.bitcoincash] as unknown as string) + v.parseAddress([exampleAddresses.ecash, exampleAddresses.bitcoincash]) }).toThrow(RESPONSE_MESSAGES.INVALID_ADDRESS_400.message) }) }) From 83d123a3496b25700ff3930cea4d3e6f12e5bb23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:58:29 +0000 Subject: [PATCH 3/3] Return 400 for repeated address query keys Co-authored-by: Klakurka <4713204+Klakurka@users.noreply.github.com> --- pages/api/address/balance/[address].ts | 5 ++- .../address/transactions/count/[address].ts | 3 ++ tests/integration-tests/api.test.ts | 35 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pages/api/address/balance/[address].ts b/pages/api/address/balance/[address].ts index 93801e956..c0b79af29 100644 --- a/pages/api/address/balance/[address].ts +++ b/pages/api/address/balance/[address].ts @@ -6,7 +6,7 @@ import { runMiddleware, satoshisToUnit } from 'utils/index' import xecaddr from 'xecaddrjs' import { multiBlockchainClient } from 'services/chronikService' -const { ADDRESS_NOT_PROVIDED_400 } = RESPONSE_MESSAGES +const { ADDRESS_NOT_PROVIDED_400, INVALID_ADDRESS_400 } = RESPONSE_MESSAGES const cors = Cors({ methods: ['GET', 'HEAD'] }) @@ -24,6 +24,9 @@ export default async (req: NextApiRequest, res: NextApiResponse): Promise case ADDRESS_NOT_PROVIDED_400.message: res.status(ADDRESS_NOT_PROVIDED_400.statusCode).json(ADDRESS_NOT_PROVIDED_400) break + case INVALID_ADDRESS_400.message: + res.status(INVALID_ADDRESS_400.statusCode).json(INVALID_ADDRESS_400) + break default: res.status(500).json({ statusCode: 500, message: err.message }) } diff --git a/pages/api/address/transactions/count/[address].ts b/pages/api/address/transactions/count/[address].ts index e7bd9a62e..a2a70bebc 100644 --- a/pages/api/address/transactions/count/[address].ts +++ b/pages/api/address/transactions/count/[address].ts @@ -20,6 +20,9 @@ export default async (req: NextApiRequest, res: NextApiResponse): Promise case RESPONSE_MESSAGES.MULTIPLE_USER_IDS_PROVIDED_400.message: res.status(400).json(RESPONSE_MESSAGES.MULTIPLE_USER_IDS_PROVIDED_400) break + case RESPONSE_MESSAGES.INVALID_ADDRESS_400.message: + res.status(RESPONSE_MESSAGES.INVALID_ADDRESS_400.statusCode).json(RESPONSE_MESSAGES.INVALID_ADDRESS_400) + break default: res.status(500).json({ statusCode: 500, message: err.message }) } diff --git a/tests/integration-tests/api.test.ts b/tests/integration-tests/api.test.ts index 2fca22012..ca3592289 100644 --- a/tests/integration-tests/api.test.ts +++ b/tests/integration-tests/api.test.ts @@ -8,6 +8,7 @@ import walletsEndpoint from 'pages/api/wallets/index' import walletEndpoint from 'pages/api/wallet/index' import walletIdEndpoint from 'pages/api/wallet/[id]' import transactionsEndpoint from 'pages/api/address/transactions/[address]' +import transactionCountEndpoint from 'pages/api/address/transactions/count/[address]' import balanceEndpoint from 'pages/api/address/balance/[address]' import dashboardEndpoint from 'pages/api/dashboard/index' import paymentsEndpoint from 'pages/api/payments/index' @@ -1285,6 +1286,24 @@ describe('GET /api/address/transactions/[address]', () => { }) }) +describe('GET /api/address/transactions/count/[address]', () => { + it('HTTP 400 when multiple address query parameters are provided', async () => { + const requestOptions: RequestOptions = { + method: 'GET' as RequestMethod, + headers: { + 'Content-Type': 'application/json' + }, + query: { + address: ['address1', 'address2'] + } + } + const res = await testEndpoint(requestOptions, transactionCountEndpoint) + const responseData = res._getJSONData() + expect(res.statusCode).toBe(RESPONSE_MESSAGES.INVALID_ADDRESS_400.statusCode) + expect(responseData.message).toBe(RESPONSE_MESSAGES.INVALID_ADDRESS_400.message) + }) +}) + describe('GET /api/address/balance/[address]', () => { const baseRequestOptions: RequestOptions = { method: 'GET' as RequestMethod, @@ -1300,6 +1319,22 @@ describe('GET /api/address/balance/[address]', () => { const responseData = res._getJSONData() expect(responseData.message).toBe(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message) }) + + it('Should return HTTP 400 when multiple address query parameters are provided', async () => { + const requestOptions: RequestOptions = { + method: 'GET' as RequestMethod, + headers: { + 'Content-Type': 'application/json' + }, + query: { + address: ['address1', 'address2'] + } + } + const res = await testEndpoint(requestOptions, balanceEndpoint) + const responseData = res._getJSONData() + expect(res.statusCode).toBe(RESPONSE_MESSAGES.INVALID_ADDRESS_400.statusCode) + expect(responseData.message).toBe(RESPONSE_MESSAGES.INVALID_ADDRESS_400.message) + }) }) describe('GET /api/payments', () => {