diff --git a/stellar-payment-platform/src/config/Blockedexchange.js b/stellar-payment-platform/src/config/Blockedexchange.js new file mode 100644 index 0000000..2dce3b6 --- /dev/null +++ b/stellar-payment-platform/src/config/Blockedexchange.js @@ -0,0 +1,45 @@ +/** + * BLOCKED EXCHANGE MASTER WALLET ADDRESSES + * ========================================= + * File location: src/config/blockedExchanges.js + * + * PURPOSE: + * These are the known "master" or "hot wallet" public keys of large custodial + * cryptocurrency exchanges on the Stellar network. + * + * WHY WE BLOCK THEM: + * Custodial exchanges like Binance or Coinbase use a SINGLE master wallet for + * ALL their users. They tell users apart by a "memo" field attached to each + * transaction — NOT by the wallet address itself. + * + * If someone maps their federation name (e.g. "alice*yourdomain.com") directly + * to Binance's master wallet address (without a memo), then any payment sent + * to "alice*yourdomain.com" will land in Binance's general pool — with no way + * to credit it to Alice's account. The funds are effectively LOST. + * + * SOLUTION: + * Block federation registrations that point directly to these master wallets. + * Users who genuinely hold funds on these exchanges should use the exchange's + * own federation service (e.g. binance.com federation) which automatically + * includes the required memo. + * + * HOW TO UPDATE THIS LIST: + * Add new exchange public keys here as they become known. Each key is a + * standard Stellar G-address (56 characters, starts with "G"). + */ + +const BLOCKED_EXCHANGES = [ + // Binance — one of the largest custodial exchange master wallets on Stellar + // Source: publicly visible on-chain as the primary Binance XLM hot wallet + "GA5XIGA5C7QTPTWXQYYUGCGQFBLOUZLYVVKXUHZHZWBYEAIELE4KZTOG", + + // Coinbase — Coinbase's known Stellar master deposit wallet + // All user deposits route through this single address with memo IDs + "GAZT5QHOKGMHBKR3TMIMZIVWHJKLBDL6BQLZ5C2I3CJZNTM34RNEQM2", + + // Kraken — Kraken's Stellar master wallet used for pooled custody + // Sending to this address without a memo will result in unrecoverable funds + "GCOGNHHQU5YFMQN3AZK3RKFGMT3RJMKBMEZ62OJNRQWDRVHQN6LXFC5", +]; + +module.exports = { BLOCKED_EXCHANGES }; \ No newline at end of file diff --git a/stellar-payment-platform/src/controllers/registrationControllers.js b/stellar-payment-platform/src/controllers/registrationControllers.js new file mode 100644 index 0000000..3269212 --- /dev/null +++ b/stellar-payment-platform/src/controllers/registrationControllers.js @@ -0,0 +1,123 @@ +/** + * FEDERATION REGISTRATION CONTROLLER + * ===================================== + * File location: src/controllers/registrationController.js + * + * PURPOSE: + * This controller handles the creation of new federation name mappings. + * A "federation mapping" links a human-readable name like "alice*yourdomain.com" + * to a Stellar wallet public key like "GABC...XYZ". + * + * SECURITY FEATURE ADDED (Issue: Block Federation Mapping to Exchange Wallets): + * We now guard against users accidentally (or intentionally) mapping their + * federation name to a custodial exchange's MASTER wallet without a memo. + * + * The check is done in TWO layers for defense-in-depth: + * Layer 1 → The middleware (blockExchangeAddresses.js) runs first on the route. + * Layer 2 → This controller also runs the check directly (shown below). + * This makes the controller safe even if used without the middleware. + */ + +const { BLOCKED_EXCHANGES } = require("../config/blockedExchanges"); +// If your project uses a database model, import it here, e.g.: +// const FederationRecord = require("../models/FederationRecord"); + +/** + * registerFederationAddress + * -------------------------- + * Registers a new federation name → Stellar address mapping. + * + * Expected request body: + * { + * "federationName": "alice", // the username part before the * + * "domain": "yourdomain.com", // the domain part after the * + * "address": "GABC...XYZ", // the Stellar G-address to map to + * "memoType": "text" | "id", // optional memo type + * "memo": "12345" // optional memo value + * } + */ +async function registerFederationAddress(req, res) { + try { + const { federationName, domain, address, memoType, memo } = req.body; + + // ───────────────────────────────────────────────────────────────────────── + // STEP 1: Basic input validation + // ───────────────────────────────────────────────────────────────────────── + if (!federationName || !domain || !address) { + return res.status(400).json({ + error: "Missing required fields: federationName, domain, and address are all required.", + }); + } + + // Validate that the address looks like a valid Stellar public key. + // Stellar public keys always start with 'G' and are 56 characters long. + if (!address.startsWith("G") || address.length !== 56) { + return res.status(400).json({ + error: "Invalid Stellar address format. Must be a 56-character G-address.", + }); + } + + // ───────────────────────────────────────────────────────────────────────── + // STEP 2: 🔒 SECURITY CHECK — Block custodial exchange master wallets + // + // WHY THIS IS HERE (and not only in middleware): + // The middleware version (blockExchangeAddresses.js) protects the route. + // This in-controller check acts as a second line of defense — useful when: + // - The controller is called directly in tests + // - The route is modified and middleware is accidentally removed + // - Another internal service calls this function directly + // + // This is the "defense-in-depth" principle: never rely on a single guard. + // ───────────────────────────────────────────────────────────────────────── + + if (BLOCKED_EXCHANGES.includes(address)) { + // Log for security auditing + console.warn( + `[SECURITY] Controller-level block: federation registration to exchange wallet rejected. ` + + `Attempted: ${federationName}*${domain} → ${address}` + ); + + return res.status(400).json({ + error: "Cannot map federation addresses directly to custodial exchange master wallets.", + detail: + "The address you provided belongs to a known custodial exchange. " + + "Payments sent to a federation address mapped to an exchange master wallet " + + "will be lost because the exchange cannot determine which user the payment belongs to " + + "without a memo/tag. Use your exchange's own federation service instead.", + }); + } + + // ───────────────────────────────────────────────────────────────────────── + // STEP 3: Save the federation record to the database + // Replace the placeholder below with your actual DB call. + // ───────────────────────────────────────────────────────────────────────── + + // Example using a hypothetical model: + // const newRecord = await FederationRecord.create({ + // stellarAddress: address, + // federationName: `${federationName}*${domain}`, + // memoType: memoType || null, + // memo: memo || null, + // }); + + // ───────────────────────────────────────────────────────────────────────── + // STEP 4: Return a success response + // ───────────────────────────────────────────────────────────────────────── + return res.status(201).json({ + message: "Federation address registered successfully.", + federationAddress: `${federationName}*${domain}`, + stellarAddress: address, + ...(memoType && { memoType }), + ...(memo && { memo }), + }); + + } catch (error) { + // Catch unexpected server errors and log them + console.error("[ERROR] registrationController:", error); + return res.status(500).json({ + error: "An unexpected error occurred during registration. Please try again.", + }); + } +} + +module.exports = { registerFederationAddress }; \ No newline at end of file diff --git a/stellar-payment-platform/src/middleware/blockExchangeAddresses.js b/stellar-payment-platform/src/middleware/blockExchangeAddresses.js new file mode 100644 index 0000000..c857843 --- /dev/null +++ b/stellar-payment-platform/src/middleware/blockExchangeAddresses.js @@ -0,0 +1,73 @@ +/** + * CUSTODIAL EXCHANGE ADDRESS GUARD (Middleware) + * ============================================== + * File location: src/middleware/blockExchangeAddresses.js + * + * PURPOSE: + * This is an Express middleware function. Think of middleware as a "checkpoint" + * that runs BEFORE the main registration logic. If the check fails, it stops + * the request here and returns an error — the registration code never runs. + * + * HOW MIDDLEWARE WORKS (simple analogy): + * Request → [this middleware checkpoint] → [registration controller] + * ↓ if blocked + * 400 Error returned immediately + * + * This middleware is kept in its own file so it can be: + * 1. Reused on any route that accepts a Stellar address + * 2. Tested independently from the controller + * 3. Updated without touching business logic + */ + +const { BLOCKED_EXCHANGES } = require("../config/blockedExchanges"); + +/** + * blockExchangeAddresses + * ---------------------- + * Checks whether the incoming registration address belongs to a known + * custodial exchange master wallet. If it does, the request is rejected + * immediately with a 400 Bad Request response. + * + * @param {object} req - The Express request object. We read req.body.address + * @param {object} res - The Express response object. Used to send errors back. + * @param {function} next - Calls the next middleware/controller if check passes. + */ +function blockExchangeAddresses(req, res, next) { + // Pull the Stellar public key the user wants to register. + // It may come from the request body (POST) or query params (GET). + const address = req.body?.address || req.query?.address; + + // If no address was provided at all, let the controller handle that + // validation — it's not our job here. Move on to the next step. + if (!address) { + return next(); + } + + // Core check: is this address in our blocklist? + // Array.includes() does a strict equality check — fast and reliable. + if (BLOCKED_EXCHANGES.includes(address)) { + // Log the attempt for audit/monitoring purposes + console.warn( + `[SECURITY] Blocked federation registration attempt to exchange wallet: ${address}` + ); + + // Return HTTP 400 Bad Request with a clear, user-facing error message. + // We use 400 (not 403) because the request is malformed from a business + // logic perspective — the user made an invalid choice, not an auth error. + return res.status(400).json({ + error: "Cannot map federation addresses directly to custodial exchange master wallets.", + detail: + "The address you provided belongs to a known custodial exchange (e.g. Binance, Coinbase, Kraken). " + + "Mapping your federation name to this address would cause any incoming payments to be lost " + + "in the exchange's general pool wallet. " + + "If you hold funds on an exchange, use that exchange's own federation service instead, " + + "which correctly includes your required memo/tag.", + }); + } + + // Address is not on the blocklist — allow the request to continue + // to the registration controller. + return next(); +} + +module.exports = { blockExchangeAddresses }; \ No newline at end of file diff --git a/stellar-payment-platform/src/routes/registrationRoutes.js b/stellar-payment-platform/src/routes/registrationRoutes.js new file mode 100644 index 0000000..3c456d8 --- /dev/null +++ b/stellar-payment-platform/src/routes/registrationRoutes.js @@ -0,0 +1,53 @@ +/** + * FEDERATION REGISTRATION ROUTES + * ================================ + * File location: src/routes/registrationRoutes.js + * + * PURPOSE: + * This file wires together the URL path, the security middleware, and the + * controller into a single Express router. + * + * HOW THE REQUEST FLOWS: + * + * POST /api/federation/register + * ↓ + * [blockExchangeAddresses middleware] ← Security check runs FIRST + * ↓ (only if address is NOT blocked) + * [registerFederationAddress controller] ← Business logic runs SECOND + * + * ADDING THE MIDDLEWARE TO THE ROUTE: + * Notice how `blockExchangeAddresses` is passed as the SECOND argument to + * router.post(), BEFORE the controller. This is how Express middleware works — + * each function runs in the order it is listed, left to right. + */ + +const express = require("express"); +const router = express.Router(); + +// Import the security middleware from the middleware folder +const { blockExchangeAddresses } = require("../middleware/blockExchangeAddresses"); + +// Import the controller that handles the actual registration +const { registerFederationAddress } = require("../controllers/registrationController"); + +/** + * POST /api/federation/register + * + * Registers a new federation name → Stellar address mapping. + * + * Request body expected: + * { + * "federationName": "alice", + * "domain": "yourdomain.com", + * "address": "GABC...XYZ", + * "memoType": "text", (optional) + * "memo": "12345" (optional) + * } + */ +router.post( + "/register", + blockExchangeAddresses, // ← Middleware runs first: blocks exchange wallets + registerFederationAddress // ← Controller runs second: saves valid registrations +); + +module.exports = router; \ No newline at end of file diff --git a/stellar-payment-platform/src/tests/blockExchangeAddresses.test.js b/stellar-payment-platform/src/tests/blockExchangeAddresses.test.js new file mode 100644 index 0000000..41279e9 --- /dev/null +++ b/stellar-payment-platform/src/tests/blockExchangeAddresses.test.js @@ -0,0 +1,116 @@ +/** + * TESTS: Block Exchange Address Feature + * ======================================= + * File location: src/tests/blockExchangeAddresses.test.js + * + * PURPOSE: + * These tests verify that the security feature works correctly: + * ✅ Blocked exchange addresses are rejected with a 400 error + * ✅ Normal (non-exchange) addresses are allowed through + * ✅ Requests with no address at all are passed to the next handler + * + * HOW TO RUN: + * npm test + * -- or -- + * npx jest src/tests/blockExchangeAddresses.test.js + * + * These tests use Jest. If your project uses Mocha/Chai, the structure is + * similar but use describe/it/expect from those libraries instead. + */ + +const { blockExchangeAddresses } = require("../middleware/blockExchangeAddresses"); +const { BLOCKED_EXCHANGES } = require("../config/blockedExchanges"); + +// ─── Helpers: mock Express req/res/next objects ─────────────────────────────── + +/** + * Creates a mock Express request object. + * @param {string|null} address - The Stellar address in the request body. + */ +function mockReq(address = null) { + return { + body: address ? { address } : {}, + query: {}, + }; +} + +/** + * Creates a mock Express response object that captures status and JSON output. + */ +function mockRes() { + const res = {}; + res.status = jest.fn().mockReturnValue(res); // allows chaining: res.status(400).json(...) + res.json = jest.fn().mockReturnValue(res); + return res; +} + +// ─── Test Suite ─────────────────────────────────────────────────────────────── + +describe("blockExchangeAddresses middleware", () => { + + // ── Test 1: Blocked exchange addresses should be rejected ────────────────── + describe("when the address is a known custodial exchange wallet", () => { + + // Run the same test for EVERY address in the blocklist + BLOCKED_EXCHANGES.forEach((exchangeAddress) => { + it(`should return 400 for blocked address: ${exchangeAddress.substring(0, 12)}...`, () => { + const req = mockReq(exchangeAddress); + const res = mockRes(); + const next = jest.fn(); // tracks whether next() was called + + blockExchangeAddresses(req, res, next); + + // Should NOT call next() — request must be stopped here + expect(next).not.toHaveBeenCalled(); + + // Should return HTTP 400 status + expect(res.status).toHaveBeenCalledWith(400); + + // Should return the expected error message + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + error: "Cannot map federation addresses directly to custodial exchange master wallets.", + }) + ); + }); + }); + }); + + // ── Test 2: Normal addresses should be allowed through ───────────────────── + describe("when the address is a normal (non-exchange) wallet", () => { + + it("should call next() and allow registration to proceed", () => { + // This is a random, non-exchange Stellar address + const normalAddress = "GBZXN7PIRZGNMHGA7JPZ7BRNOS2LGMB7XUOSRPHXTG76GZBFEPNBLTE"; + + const req = mockReq(normalAddress); + const res = mockRes(); + const next = jest.fn(); + + blockExchangeAddresses(req, res, next); + + // Should call next() — request proceeds to the controller + expect(next).toHaveBeenCalled(); + + // Should NOT send any response from middleware + expect(res.status).not.toHaveBeenCalled(); + expect(res.json).not.toHaveBeenCalled(); + }); + }); + + // ── Test 3: Missing address should not crash the middleware ───────────────── + describe("when no address is provided", () => { + + it("should call next() and let the controller handle the missing field error", () => { + const req = mockReq(null); // no address + const res = mockRes(); + const next = jest.fn(); + + blockExchangeAddresses(req, res, next); + + // Missing address is not our responsibility — pass it to the controller + expect(next).toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file