Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions stellar-payment-platform/src/config/Blockedexchange.js
Original file line number Diff line number Diff line change
@@ -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 };
123 changes: 123 additions & 0 deletions stellar-payment-platform/src/controllers/registrationControllers.js
Original file line number Diff line number Diff line change
@@ -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 };
73 changes: 73 additions & 0 deletions stellar-payment-platform/src/middleware/blockExchangeAddresses.js
Original file line number Diff line number Diff line change
@@ -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 };
53 changes: 53 additions & 0 deletions stellar-payment-platform/src/routes/registrationRoutes.js
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading