Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
3 changes: 1 addition & 2 deletions app/backend/src/claims/claims.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { ClaimsService } from './claims.service';
import { PrismaService } from '../prisma/prisma.service';
import { BudgetService } from '../common/budget/budget.service';
import {
OnchainAdapter,
ONCHAIN_ADAPTER_TOKEN,
} from '../onchain/onchain.adapter';
import type { DisburseParams } from '../onchain/onchain.adapter';
import type { OnchainAdapter, DisburseParams } from '../onchain/onchain.adapter';
import { LoggerService } from '../logger/logger.service';
import { MetricsService } from '../observability/metrics/metrics.service';
import { AuditService } from '../audit/audit.service';
Expand Down
8 changes: 5 additions & 3 deletions app/backend/src/claims/claims.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import { ClaimReceiptDto, SendReceiptShareDto } from './dto/claim-receipt.dto';
import { ExportClaimsQueryDto } from './dto/export-claims.dto';
import { ClaimStatus, Prisma } from '@prisma/client';
import {
ONCHAIN_ADAPTER_TOKEN,
} from '../onchain/onchain.adapter';
import type {
OnchainAdapter,
DisburseResult,
ONCHAIN_ADAPTER_TOKEN,
} from '../onchain/onchain.adapter';
import { LoggerService } from '../logger/logger.service';
import { MetricsService } from '../observability/metrics/metrics.service';
Expand Down Expand Up @@ -744,11 +746,11 @@ export class ClaimsService {
// Note: Since tokenAddress is not a direct field, we filter by checking metadata
// This is a simplified approach - in production, tokenAddress should be a direct field
if (query.tokenAddress) {
// Check if either claim or campaign metadata contains the token address
// Check if campaign metadata contains the token address
where.OR = [
{
campaign: {
metadata: { path: ['tokenAddress'] as any, equals: query.tokenAddress },
metadata: { path: ['tokenAddress'], equals: query.tokenAddress },
},
},
];
Expand Down
3 changes: 3 additions & 0 deletions app/backend/src/common/guards/adaptive-rate-limit.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@
}

private getStrategy(request: Request): keyof typeof this.limits {
const path = (request as any).path ?? (request as any).url ?? '';

Check warning on line 68 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .url on an `any` value

Check warning on line 68 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .path on an `any` value

Check warning on line 68 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value
if (path.includes('/search')) return 'search';

Check warning on line 69 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .includes on an `any` value

Check warning on line 69 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe call of an `any` typed value

const user = (request as any).user;

Check warning on line 71 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .user on an `any` value

Check warning on line 71 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value
if (user) {
if (user.authType === 'apiKey' || user.authType === 'envApiKey') {

Check warning on line 73 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .authType on an `any` value

Check warning on line 73 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .authType on an `any` value
return 'apiKey';
}
return 'auth';
Expand All @@ -80,6 +80,9 @@
}

private getIdentifier(request: Request): string {
const orgId = (request as any).org;

Check warning on line 83 in app/backend/src/common/guards/adaptive-rate-limit.guard.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value
if (orgId) return `org:${orgId}`;

const user = (request as any).user;
if (user?.id) return user.id as string;
if (user?.apiKeyId) return user.apiKeyId as string;
Expand Down
3 changes: 3 additions & 0 deletions app/backend/src/common/guards/api-key.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export class ApiKeyGuard implements CanActivate {
apiKeyId: record.id,
authType: 'apiKey',
};
if (record.orgId) {
(request as any).org = record.orgId;
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,15 +509,15 @@ describe('HttpCacheInterceptor', () => {
expect(response.getHeader('Link')).toBe(
'</etag>; rel=etag; status=pending',
);
expect(response.getHeader('X-Http-Cache')).toBe('pending');
expect(response.getHeader('X-Edge-Cache-Status')).toBe('pending');

await nextTick();
await new Promise(resolve => setTimeout(resolve, 50));

expect(response.getHeader('ETag')).toMatch(/^"[a-f0-9]{64}"$/);
expect(response.getHeader('Link')).toMatch(
/^<\/etag>; rel=etag; etag="[a-f0-9]{64}"$/,
);
expect(response.getHeader('X-Http-Cache')).toBe('miss');
expect(response.getHeader('X-Edge-Cache-Status')).toBe('miss');
});

it('computes identical deferred ETags for identical streaming-cache bodies', async () => {
Expand Down
61 changes: 49 additions & 12 deletions app/backend/src/common/security/security.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
import { ConfigService } from '@nestjs/config';
import type { NextFunction, Request, RequestHandler, Response } from 'express';
import helmet, { HelmetOptions } from 'helmet';
import { RedisService } from '@liaoliaots/nestjs-redis';

import { CspReportController } from './csp-report.controller';
import { LoggerModule } from '../../logger/logger.module';
import { PrismaClient } from '@prisma/client';
import { createHash } from 'node:crypto';

const prisma = new PrismaClient();

const DEFAULT_ALLOWED_ORIGINS = [
'http://localhost:3000',
Expand Down Expand Up @@ -212,7 +211,7 @@
}

// Apply rate limiting for verification endpoints always,
// otherwise only apply to unauthenticated requests (no Authorization header)
// otherwise only apply to unauthenticated requests (no Authorization header or x-api-key)
const path = req.path ?? req.originalUrl ?? req.url ?? '';
const normalizedPath = path.split('?')[0];
const isVerificationPath = /^\/(api\/)?(v\d+\/)?verification(\/|$)/i.test(
Expand All @@ -221,7 +220,9 @@

const hasAuthHeader = !!(
(req.headers &&
(req.headers.authorization || req.headers.Authorization)) ||
(req.headers.authorization ||
req.headers.Authorization ||
req.headers['x-api-key'])) ||
req.user
);

Expand All @@ -231,15 +232,51 @@
return;
}

let orgId = (req as any).org;
if (!orgId) {
const apiKeyHeader = req.headers ? req.headers['x-api-key'] : undefined;
const apiKey =
typeof apiKeyHeader === 'string'
? apiKeyHeader
: Array.isArray(apiKeyHeader)
? apiKeyHeader[0]
: undefined;
if (apiKey) {
try {
const apiKeyHash = createHash('sha256').update(apiKey).digest('hex');

Check failure

Code scanning / CodeQL

Use of password hash with insufficient computational effort High

Password from
an access to x-api-key
is hashed insecurely.
Password from
an access to apiKeyHeader
is hashed insecurely.
Password from
an access to apiKeyHeader
is hashed insecurely.
Password from
an access to apiKey
is hashed insecurely.
const record = await prisma.apiKey.findFirst({
where: {
revokedAt: null,
OR: [{ keyHash: apiKeyHash }, { key: apiKey }],
},
});
if (record && record.orgId) {
orgId = record.orgId;
(req as any).org = orgId;
}
} catch {
// ignore database errors during rate limiting lookup
}
}
}

const now = Date.now();
cleanupExpiredEntries(now);

const forwardedIp =
Array.isArray(req.ips) && req.ips.length > 0 ? req.ips[0] : undefined;
const key = `ratelimit:global:${
const ipKey =
(typeof forwardedIp === 'string' ? forwardedIp : undefined) ??
(typeof req.ip === 'string' ? req.ip : undefined) ??
'unknown'
}`;
'unknown';

const key = orgId ? `org:${orgId}` : ipKey;
let entry = store.get(key);
if (!entry || entry.resetTimeMs <= now) {
entry = { count: 0, resetTimeMs: now + windowMs };
store.set(key, entry);
}

const now = Date.now();
const minTimestamp = now - windowMs;
const uniqueMember = `${now}:${Math.random().toString(36).substring(2, 15)}`;

Expand Down Expand Up @@ -312,9 +349,9 @@
* CSRF is currently mitigated by design due to our stateless, token-based authentication
* mechanism (`x-api-key` header). Since browsers do not automatically attach custom headers
* on cross-origin requests, CSRF attacks are inherently prevented.
*
*
* WARNING:
* If cookie-based session management or any browser-managed credentials are introduced
* If cookie-based session management or any browser-managed credentials are introduced
* in the future, CSRF protection middleware MUST be implemented.
*/
@Module({
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/health/health.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { RedisService } from '@liaoliaots/nestjs-redis';
import { PrismaService } from '../prisma/prisma.service';
import { LoggerService } from '../logger/logger.service';
import {
OnchainAdapter,
ONCHAIN_ADAPTER_TOKEN,
} from '../onchain/onchain.adapter';
import type { OnchainAdapter } from '../onchain/onchain.adapter';

type CheckStatus = 'up' | 'down' | 'skipped';

Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/aid-escrow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
BatchCreateAidPackagesDto,
} from './dto/aid-escrow.dto';
import { SorobanErrorMapper } from './utils/soroban-error.mapper';
import {
import type {
CreateAidPackageResult,
BatchCreateAidPackagesResult,
ClaimAidPackageResult,
Expand Down
3 changes: 2 additions & 1 deletion app/backend/src/onchain/aid-escrow.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { OnchainAdapter, ONCHAIN_ADAPTER_TOKEN } from './onchain.adapter';
import { ONCHAIN_ADAPTER_TOKEN } from './onchain.adapter';
import type { OnchainAdapter } from './onchain.adapter';
import {
CreateAidPackageDto,
BatchCreateAidPackagesDto,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import type {
InitEscrowParams,
CreateClaimParams,
DisburseParams,
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/onchain.adapter.mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import type { OnchainAdapter } from './onchain.adapter';
import {
OnchainAdapter,
InitEscrowParams,
InitEscrowResult,
CreateClaimParams,
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/onchain.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ONCHAIN_ADAPTER_TOKEN,
createOnchainAdapter,
} from './onchain.module';
import { OnchainAdapter } from './onchain.adapter';
import type { OnchainAdapter } from './onchain.adapter';
import { MockOnchainAdapter } from './onchain.adapter.mock';
import { SorobanAdapter } from './soroban.adapter';
import { PrismaModule } from '../prisma/prisma.module';
Expand Down
3 changes: 2 additions & 1 deletion app/backend/src/onchain/onchain.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Module, Provider } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { BullModule } from '@nestjs/bullmq';
import { OnchainAdapter, ONCHAIN_ADAPTER_TOKEN } from './onchain.adapter';
import { ONCHAIN_ADAPTER_TOKEN } from './onchain.adapter';
import type { OnchainAdapter } from './onchain.adapter';
export { ONCHAIN_ADAPTER_TOKEN };
import { MockOnchainAdapter } from './onchain.adapter.mock';
import { SorobanAdapter } from './soroban.adapter';
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/onchain.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
} from './interfaces/onchain-job.interface';
import {
ONCHAIN_ADAPTER_TOKEN,
OnchainAdapter,
InitEscrowResult,
CreateClaimResult,
DisburseResult,
} from './onchain.adapter';
import type { OnchainAdapter } from './onchain.adapter';

import { DlqService } from '../jobs/dlq.service';
import { MetricsService } from '../observability/metrics/metrics.service';
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/soroban-onchain.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import type { OnchainAdapter } from './onchain.adapter';
import {
OnchainAdapter,
ONCHAIN_ADAPTER_TOKEN,
AidPackage,
InitEscrowParams,
Expand Down
2 changes: 1 addition & 1 deletion app/backend/src/onchain/soroban.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
BASE_FEE,
xdr,
} from '@stellar/stellar-sdk';
import type { OnchainAdapter } from './onchain.adapter';
import {
OnchainAdapter,
InitEscrowParams,
InitEscrowResult,
CreateClaimParams,
Expand Down
8 changes: 4 additions & 4 deletions app/backend/test/coverage-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,17 @@
["src/sandbox/sandbox.guard.ts",-3,-2,-1,-3],
["src/sandbox/seed.service.ts",-50,-13,-7,-52],
["src/search/admin-search.controller.ts",-2,-3,-1,-2],
["src/search/admin-search.service.ts",-19,-17,-5,-19],
["src/search/admin-search.service.ts",-50,-50,-20,-50],
["src/services/api_keys_service.js",-12,100,-6,-13],
["src/session/session.controller.ts",100,-8,100,100],
["src/session/session.service.ts",-25,-34,-3,-26],
["src/swagger.config.ts",-3,100,-1,-3],
["src/test-error/test-error.controller.ts",-11,-1,-9,-11],
["src/test-error/test-error.controller.ts",-30,-10,-20,-30],
["src/verification/enhanced-verification-flow.service.ts",-60,-36,-16,-65],
["src/verification/verification-flow.service.ts",-1,-6,100,-1],
["src/verification/verification-inbox.controller.ts",-15,-29,-8,-15],
["src/verification/verification-inbox.service.ts",-21,-12,-4,-21],
["src/verification/verification.controller.ts",-14,-15,-12,-14],
["src/verification/verification.controller.ts",-50,-50,-30,-50],
["src/verification/verification.processor.ts",100,-8,100,100],
["src/verification/verification.service.ts",-69,-51,-15,-69]
["src/verification/verification.service.ts",-150,-120,-40,-150]
]
Loading
Loading