From 7ac437c4be4876bc610e27b78fe0b64aa605091f Mon Sep 17 00:00:00 2001 From: Gandalf Date: Tue, 30 Dec 2025 12:01:34 +0100 Subject: [PATCH] fix: Add username validation to avatar endpoints Add Hive account name validation to avatar proxy endpoints in both blog and wallet apps. Invalid usernames now return 400 Bad Request instead of being passed to the image host. Validation uses wax library with regex fallback for thread-safety. The validation logic is centralized in @hive/transaction package and re-exported from apps/blog/utils/validate-links.ts for backwards compatibility. --- apps/blog/app/api/avatar/route.ts | 5 ++ apps/blog/utils/validate-links.ts | 71 +------------------ apps/wallet/pages/api/avatar.ts | 5 ++ packages/transaction/index.ts | 2 + .../transaction/lib/validate-hive-account.ts | 65 +++++++++++++++++ 5 files changed, 80 insertions(+), 68 deletions(-) create mode 100644 packages/transaction/lib/validate-hive-account.ts diff --git a/apps/blog/app/api/avatar/route.ts b/apps/blog/app/api/avatar/route.ts index de92ebd92..838c1380a 100644 --- a/apps/blog/app/api/avatar/route.ts +++ b/apps/blog/app/api/avatar/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { configuredImagesEndpoint } from '@hive/ui/config/public-vars'; +import { isHiveAccountNameValid } from '@hive/transaction'; /** * Proxy endpoint for user avatars that prevents caching @@ -18,6 +19,10 @@ export async function GET(req: NextRequest): Promise { return NextResponse.json({ error: 'Username is required' }, { status: 400 }); } + if (!(await isHiveAccountNameValid(username))) { + return NextResponse.json({ error: 'Invalid username' }, { status: 400 }); + } + // Build the image hoster URL let imageUrl: string; if (size && ['small', 'medium', 'large'].includes(size)) { diff --git a/apps/blog/utils/validate-links.ts b/apps/blog/utils/validate-links.ts index 68b701654..60d7e6253 100644 --- a/apps/blog/utils/validate-links.ts +++ b/apps/blog/utils/validate-links.ts @@ -1,74 +1,9 @@ -import { getChain } from '@hive/transaction/lib/chain'; -import { getLogger } from '@ui/lib/logging'; - -const logger = getLogger('app'); +import { isHiveAccountNameValid } from '@hive/transaction'; export function isPermlinkValid(permlink: string): boolean { if (typeof permlink !== 'string') return false; return /^[a-z0-9-]{1,255}$/.test(permlink); } -/** - * Validates Hive account names using wax library with regex fallback. - * - * TEMPORARY WORKAROUND: The wax WASM module isn't thread-safe - concurrent - * Server Component requests cause "memory access out of bounds" errors. - * We catch WASM errors and fall back to regex validation until wax is fixed. - * - * @see https://gitlab.syncad.com/hive/denser/-/issues/758 - * @see https://gitlab.syncad.com/hive/wax/-/issues/140 - * - * TODO: Remove regex fallback once wax thread-safety is resolved - */ -export async function isUsernameValid(accountName: string): Promise { - if (typeof accountName !== 'string') return false; - - try { - const chain = await getChain(); - return chain.isValidAccountName(accountName); - } catch (error) { - // TEMPORARY: Log WASM failure and fall back to regex validation - // See: https://gitlab.syncad.com/hive/wax/-/issues/140 - logger.warn({ err: error, accountName }, 'WASM isValidAccountName failed, using regex fallback'); - return isUsernameValidRegex(accountName); - } -} - -/** - * Regex fallback for Hive account name validation. - * Used when wax WASM fails due to concurrent access. - * - * Rules based on Hive blockchain consensus: - * - 3-16 characters - * - Lowercase letters, numbers, dots, hyphens only - * - Must start with a letter - * - Cannot end with hyphen or dot - * - Each segment (separated by dots) must start with a letter - * - No consecutive dots or hyphens - * - * TODO: Remove once wax thread-safety is resolved (see wax#140) - */ -function isUsernameValidRegex(accountName: string): boolean { - if (accountName.length < 3 || accountName.length > 16) return false; - - // Must contain only lowercase letters, numbers, dots, and hyphens - if (!/^[a-z0-9.-]+$/.test(accountName)) return false; - - // Must start with a letter - if (!/^[a-z]/.test(accountName)) return false; - - // Cannot end with hyphen or dot - if (/[.-]$/.test(accountName)) return false; - - // No consecutive dots or hyphens - if (/[.-]{2}/.test(accountName)) return false; - - // Each segment must start with a letter - const segments = accountName.split('.'); - for (const segment of segments) { - if (segment.length === 0) return false; - if (!/^[a-z]/.test(segment)) return false; - } - - return true; -} +// Re-export from @hive/transaction for backwards compatibility +export { isHiveAccountNameValid as isUsernameValid }; diff --git a/apps/wallet/pages/api/avatar.ts b/apps/wallet/pages/api/avatar.ts index fd3eeddaf..f017c938b 100644 --- a/apps/wallet/pages/api/avatar.ts +++ b/apps/wallet/pages/api/avatar.ts @@ -1,5 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { configuredImagesEndpoint } from '@hive/ui/config/public-vars'; +import { isHiveAccountNameValid } from '@hive/transaction'; type ResponseData = { error?: string; @@ -25,6 +26,10 @@ export default async function handler( return res.status(400).json({ error: 'Username is required' }); } + if (!(await isHiveAccountNameValid(username))) { + return res.status(400).json({ error: 'Invalid username' }); + } + // Build the image hoster URL let imageUrl: string; if (size && typeof size === 'string' && ['small', 'medium', 'large'].includes(size)) { diff --git a/packages/transaction/index.ts b/packages/transaction/index.ts index 903b97533..c058c5d13 100644 --- a/packages/transaction/index.ts +++ b/packages/transaction/index.ts @@ -1158,3 +1158,5 @@ export class TransactionService { } } export const transactionService = new TransactionService(); + +export { isHiveAccountNameValid, isHiveAccountNameValidFallback } from './lib/validate-hive-account'; diff --git a/packages/transaction/lib/validate-hive-account.ts b/packages/transaction/lib/validate-hive-account.ts new file mode 100644 index 000000000..a3a485eb9 --- /dev/null +++ b/packages/transaction/lib/validate-hive-account.ts @@ -0,0 +1,65 @@ +import { getChain } from './chain'; +import { getLogger } from '@hive/ui/lib/logging'; + +const logger = getLogger('validate-hive-account'); + +/** + * Regex fallback for Hive account name validation. + * Used when wax WASM fails due to concurrent access. + * + * NOTE: This regex is NOT an accurate implementation of Hive account name + * validation rules. It's intentionally permissive as a fallback when wax + * is unavailable. Some technically invalid names may pass this check, but + * they will be safely rejected at a later stage (e.g., by the Hive API or + * image host returning 404). This is just cheap/fast input sanitization. + * + * @see https://gitlab.syncad.com/hive/wax/-/issues/140 + */ +export function isHiveAccountNameValidFallback(accountName: string): boolean { + if (accountName.length < 3 || accountName.length > 16) return false; + + // Must contain only lowercase letters, numbers, dots, and hyphens + if (!/^[a-z0-9.-]+$/.test(accountName)) return false; + + // Must start with a letter + if (!/^[a-z]/.test(accountName)) return false; + + // Cannot end with hyphen or dot + if (/[.-]$/.test(accountName)) return false; + + // No consecutive dots (consecutive hyphens ARE valid, e.g., 'a--a') + if (/\.\./.test(accountName)) return false; + + // Each segment (separated by dots) must start with a letter + const segments = accountName.split('.'); + for (const segment of segments) { + if (segment.length === 0) return false; + if (!/^[a-z]/.test(segment)) return false; + } + + return true; +} + +/** + * Validates Hive account names using wax library with regex fallback. + * + * TEMPORARY WORKAROUND: The wax WASM module isn't thread-safe - concurrent + * Server Component requests cause "memory access out of bounds" errors. + * We catch WASM errors and fall back to regex validation until wax is fixed. + * + * @see https://gitlab.syncad.com/hive/denser/-/issues/758 + * @see https://gitlab.syncad.com/hive/wax/-/issues/140 + * + * TODO: Remove regex fallback once wax thread-safety is resolved + */ +export async function isHiveAccountNameValid(accountName: string): Promise { + if (typeof accountName !== 'string') return false; + + try { + const chain = await getChain(); + return chain.isValidAccountName(accountName); + } catch (error) { + logger.warn({ err: error, accountName }, 'WASM isValidAccountName failed, using regex fallback'); + return isHiveAccountNameValidFallback(accountName); + } +} -- GitLab