Skip to content

bug: userId not validated as Stellar address in notification endpoints — database accepts arbitrary strings #7

Description

@Uchechukwu-Ekezie

Description

The POST /api/notifications/subscribe, GET /api/notifications/preferences/:userId, and DELETE /api/notifications/unsubscribe endpoints accept any non-empty string as userId with no validation that it is a valid Stellar public key. This allows invalid user IDs to persist permanently in the database, breaking downstream functionality that assumes userId is a valid Stellar address.

Steps to Reproduce

# Subscribe with a completely invalid userId
curl -X POST http://localhost:3001/api/notifications/subscribe \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "not-a-stellar-address",
    "emailEnabled": true,
    "emailAddress": "user@example.com",
    "events": ["rebalance"]
  }'

# Returns: {"success": true, "message": "Notification preferences saved"}
# The string "not-a-stellar-address" is now permanently in the database

# Can retrieve preferences for the invalid ID
curl http://localhost:3001/api/notifications/preferences/not-a-stellar-address
# Returns: {"success": true, "preferences": {...}}

Root Cause

backend/src/api/routes.ts, the subscribe route (~line 591):

const { userId, emailEnabled, emailAddress, webhookEnabled, webhookUrl, events } = req.body

if (!userId) {
    return res.status(400).json({ success: false, error: 'userId is required' })
}
// ↑ Only falsy check — any non-empty string passes

The code validates emailAddress format (valid email regex) and webhookUrl format (http/https pattern), but applies no format validation to userId itself.

Downstream Impact

  1. Auto-rebalancer: When queuing notifications for a rebalance event, the service iterates users from notification_preferences. For a row with userId = "not-a-stellar-address", any attempt to look up that user's portfolio on Stellar Horizon returns an error. Depending on error handling, this could halt the entire notification pass.

  2. Stellar signature verification: The requireAuth middleware (middleware/auth.ts) verifies that a request is signed by the wallet matching userId. With an invalid Stellar address, Keypair.fromPublicKey(userId) throws a StellarBase.InvalidChecksumError, causing an unhandled exception in the middleware.

  3. Database integrity: The notification_preferences table likely has no CHECK constraint on user_id format. Over time, the table accumulates orphaned rows with no corresponding valid Stellar account.

Proposed Fix

Add Stellar address validation before persisting notification preferences:

// backend/src/api/routes.ts — notification/subscribe handler

// Stellar public key: 56 characters, starts with G (mainnet) or T (testnet)
const STELLAR_PUBLIC_KEY_REGEX = /^[GT][A-Z0-9]{54}$/

if (!STELLAR_PUBLIC_KEY_REGEX.test(userId)) {
    return res.status(400).json({
        success: false,
        error: 'userId must be a valid Stellar public key (56 characters, starting with G or T)'
    })
}

Apply the same check to GET /api/notifications/preferences/:userId (route parameter) and DELETE /api/notifications/unsubscribe.

Additionally, consider adding a CHECK constraint to the notification_preferences table:

ALTER TABLE notification_preferences
    ADD CONSTRAINT valid_stellar_address
    CHECK (user_id ~ '^[GT][A-Z0-9]{54}$');

Files Affected

  • backend/src/api/routes.ts — subscribe, preferences, unsubscribe handlers
  • backend/src/db/migrations/ — optional: add CHECK constraint
  • backend/src/api/validation.ts — add stellarAddressSchema to Zod schemas for reuse

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official Campaign

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions