initrftsebfvgyhloutersvbhustdr

This commit is contained in:
ZOMBIIIIIII
2026-05-28 22:40:36 +03:00
parent 444030e424
commit 4c00c6ca1b
4 changed files with 64 additions and 6 deletions

View File

@@ -10,6 +10,7 @@ import { authMiddleware } from './middleware/auth';
import { csrfMiddleware } from './middleware/csrf';
import { globalLimiter, mutateLimiter, sensitiveLimiter, mnemonicRevealLimiter } from './middleware/rate-limit';
import { errorHandler } from './middleware/error-handler';
import { generateCsrfToken } from './services/csrf.service';
import walletRoutes from './routes/wallet.routes';
import relayProxyRoutes from './routes/relay-proxy.routes';
import jumperProxyRoutes from './routes/jumper-proxy.routes';
@@ -41,6 +42,7 @@ app.use(
origin: corsIsWildcard ? '*' : (corsOrigins.length > 0 ? corsOrigins : false),
// Wildcard incompatible с credentials per browser spec — force false при wildcard.
credentials: corsIsWildcard ? false : env.cors.allowCredentials,
exposedHeaders: ['X-CSRF-Token', 'X-Trace-ID'],
}),
);
app.use(express.json({ limit: '64kb' })); // защита от больших payload-DoS
@@ -65,6 +67,30 @@ app.get('/api/health', async (_req, res) => {
// ── Глобальный rate limit на /api/* — ДО docs чтобы не было unauthenticated DoS на swagger.json
app.use('/api', globalLimiter);
app.get('/api/csrf', (_req, res) => {
if (!env.vault.csrfPath) {
res.json({ success: true, data: { enabled: false, csrfToken: null } });
return;
}
try {
const { token, maxAgeSec } = generateCsrfToken();
const crossSiteCookie = !corsIsWildcard && env.cors.allowCredentials;
res.cookie('csrf_token', token, {
httpOnly: false,
secure: crossSiteCookie,
sameSite: crossSiteCookie ? 'none' : 'lax',
maxAge: maxAgeSec * 1000,
path: '/',
});
res.setHeader('Cache-Control', 'no-store');
res.setHeader('X-CSRF-Token', token);
res.json({ success: true, data: { enabled: true, csrfToken: token, maxAgeSec } });
} catch (err: any) {
res.status(503).json({ success: false, error: err.message || 'CSRF token unavailable' });
}
});
// H1 — Swagger gated. В production требуется basic-auth ИЛИ NODE_ENV != production.
// JSON endpoint ОБЯЗАТЕЛЬНО до app.use('/api/docs', ...) — иначе swagger-ui-express
// перехватывает все /api/docs/* и возвращает HTML вместо JSON.

View File

@@ -209,6 +209,7 @@ const ALLOWED_TRC_FUNCTIONS = new Set<string>([
'allowance(address,address)',
'swapExactETHForTokens(uint256,address[],address,uint256)',
'swapExactTokensForETH(uint256,uint256,address[],address,uint256)',
'swapExactTokensForETHSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256)',
'swapNativeWithFee(bytes)',
'swapTokenWithFee(address,uint256,bytes)',
'getAmountsOut(uint256,address[])',

View File

@@ -36,6 +36,10 @@ export function isCsrfConfigured(): boolean {
return current !== null;
}
function b64urlEncode(buf: Buffer): string {
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
/**
* Pre-fetch CSRF config из Vault — НЕ мутирует глобал, возвращает новый объект.
*/
@@ -95,11 +99,38 @@ function decodeTimestamp(encoded: string): number {
return ts + ITSDANGEROUS_EPOCH;
}
function encodeTimestamp(unixSeconds: number): string {
let ts = unixSeconds - ITSDANGEROUS_EPOCH;
const bytes: number[] = [];
do {
bytes.unshift(ts & 0xff);
ts = Math.floor(ts / 256);
} while (ts > 0);
return b64urlEncode(Buffer.from(bytes));
}
export interface CsrfVerifyResult {
valid: boolean;
reason?: string;
}
export function generateCsrfToken(): { token: string; maxAgeSec: number } {
if (!current) {
throw new Error('CSRF secret not loaded');
}
const payload = b64urlEncode(crypto.randomBytes(32));
const timestamp = encodeTimestamp(Math.floor(Date.now() / 1000));
const payloadTs = `${payload}.${timestamp}`;
const derived = deriveKey(current.secret, current.salt, current.digest);
const signature = b64urlEncode(crypto.createHmac(current.digest, derived).update(payloadTs).digest());
return {
token: `${payloadTs}.${signature}`,
maxAgeSec: current.maxAgeSec,
};
}
export function verifyCsrfToken(token: string): CsrfVerifyResult {
if (!current) return { valid: false, reason: 'CSRF secret not loaded' };
if (!token || typeof token !== 'string') return { valid: false, reason: 'Empty token' };

View File

@@ -526,7 +526,7 @@ const TRX_BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrs
// FeeSwapRouter работает с supporting-fee variant — это уже доказано в production traffic.
const SEL_APPROVE = '095ea7b3';
const SEL_SWAP_EXACT_ETH_FOR_TOKENS = 'b6f9de95';
const SEL_SWAP_EXACT_TOKENS_FOR_ETH = '18cbafe5';
const SEL_SWAP_EXACT_TOKENS_FOR_ETH_SUPPORTING_FEE = '791ac947';
const SEL_SWAP_NATIVE_WITH_FEE = '152dad1d';
const SEL_SWAP_TOKEN_WITH_FEE = 'e8d1f203';
@@ -617,8 +617,8 @@ function buildSwapExactETHForTokensCalldata(
encAddr(to) + encU256(deadline) + pathLen + pathElements;
}
// function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)
function buildSwapExactTokensForETHCalldata(
// function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)
function buildSwapExactTokensForETHSupportingFeeCalldata(
amountIn: bigint,
amountOutMin: bigint,
path: string[],
@@ -628,7 +628,7 @@ function buildSwapExactTokensForETHCalldata(
const offsetToPath = encU256(160n); // 5 × 32 bytes
const pathLen = encU256(BigInt(path.length));
const pathElements = path.map(encAddr).join('');
return SEL_SWAP_EXACT_TOKENS_FOR_ETH + encU256(amountIn) + encU256(amountOutMin) +
return SEL_SWAP_EXACT_TOKENS_FOR_ETH_SUPPORTING_FEE + encU256(amountIn) + encU256(amountOutMin) +
offsetToPath + encAddr(to) + encU256(deadline) + pathLen + pathElements;
}
@@ -936,7 +936,7 @@ export async function quoteTrx(p: QuoteTrxParams): Promise<SwapQuoteRaw> {
* Поддерживает только TRX↔USDT.
*
* TRX → USDT (1 tx): swapNativeWithFee wraps swapExactETHForTokens.
* USDT → TRX (1-2 tx): optional approve(infinite) + swapTokenWithFee wraps swapExactTokensForETH.
* USDT → TRX (1-2 tx): optional approve(infinite) + swapTokenWithFee wraps supporting-fee swapExactTokensForETH.
*
* Slippage: если `lockedMinOut` задан → используется. Иначе re-quote on-chain.
*/
@@ -1059,7 +1059,7 @@ export async function executeTrx(
}
// Step 2: build swapTokenWithFee(USDT, amount, calldata).
const sunswapCalldata = buildSwapExactTokensForETHCalldata(
const sunswapCalldata = buildSwapExactTokensForETHSupportingFeeCalldata(
swapAmount, amountOutMin, path, fromTronAddr, deadline,
);
const tokenInEnc = encAddr(USDT_CONTRACT);