Skip to content

Commit 595f562

Browse files
Merge pull request #1360 from Hassan-oladipupo/feature/backend-security-improvements
Merging per repo maintainer review. Real security fixes (info-disclosure in exception filter, CSV/Excel formula-injection mitigation, real DB health check). Pre-existing CI failures predate this PR.
2 parents 958240f + d089442 commit 595f562

7 files changed

Lines changed: 107 additions & 19 deletions

File tree

backend/src/common/filters/all-exceptions.filter.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ export class AllExceptionsFilter implements ExceptionFilter {
2828
let message = 'Internal server error';
2929
let error = 'InternalServerError';
3030

31-
if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
32-
message = exceptionResponse.message || message;
33-
error = exceptionResponse.error || error;
34-
} else if (typeof exceptionResponse === 'string') {
35-
message = exceptionResponse;
36-
} else if (exception instanceof Error) {
37-
message = exception.message;
31+
if (exception instanceof HttpException) {
32+
if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
33+
message = exceptionResponse.message || message;
34+
error = exceptionResponse.error || error;
35+
} else if (typeof exceptionResponse === 'string') {
36+
message = exceptionResponse;
37+
}
3838
}
3939

4040
this.logger.error(

backend/src/common/services/export.service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,16 @@ export class ExportService {
7575
}));
7676

7777
for await (const item of dataStream) {
78-
worksheet.addRow(item).commit();
78+
const safeRow: Record<string, any> = {};
79+
for (const col of columns) {
80+
const val = item[col.key];
81+
if (typeof val === 'string' && /^[=+\-@\t\r]/.test(val)) {
82+
safeRow[col.key] = `'${val}`;
83+
} else {
84+
safeRow[col.key] = val;
85+
}
86+
}
87+
worksheet.addRow(safeRow).commit();
7988
}
8089

8190
worksheet.commit();
@@ -84,11 +93,18 @@ export class ExportService {
8493

8594
/**
8695
* Escapes values containing commas, quotes, or newlines according to CSV standards.
96+
* Also neutralizes formula injection by prefixing dangerous characters.
8797
*/
8898
private escapeCsvField(val: any): string {
8999
if (val === null || val === undefined) return '""';
90100

91101
let str = typeof val === 'object' ? JSON.stringify(val) : String(val);
102+
103+
// Neutralize formula injection: prefix values starting with =, +, -, @
104+
if (/^[=+\-@\t\r]/.test(str)) {
105+
str = `'${str}`;
106+
}
107+
92108
if (
93109
str.includes('"') ||
94110
str.includes(',') ||

backend/src/health/health.controller.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { Controller, Get } from '@nestjs/common';
2-
import { ApiTags, ApiOperation } from '@nestjs/swagger';
1+
import { Controller, Get, HttpCode, HttpStatus } from '@nestjs/common';
2+
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
3+
import { DataSource } from 'typeorm';
4+
import { InjectDataSource } from '@nestjs/typeorm';
35

46
@ApiTags('health')
57
@Controller('health')
68
export class HealthController {
9+
constructor(
10+
@InjectDataSource()
11+
private readonly dataSource: DataSource,
12+
) {}
13+
714
@Get('live')
815
@ApiOperation({ summary: 'Liveness check' })
916
getLiveness() {
@@ -12,12 +19,26 @@ export class HealthController {
1219

1320
@Get('ready')
1421
@ApiOperation({ summary: 'Readiness check' })
15-
getReadiness() {
22+
@ApiResponse({ status: 200, description: 'All systems operational' })
23+
@ApiResponse({ status: 503, description: 'One or more systems degraded' })
24+
@HttpCode(HttpStatus.OK)
25+
async getReadiness() {
26+
const checks = {
27+
database: 'down' as 'up' | 'down',
28+
};
29+
30+
try {
31+
await this.dataSource.query('SELECT 1');
32+
checks.database = 'up';
33+
} catch {
34+
checks.database = 'down';
35+
}
36+
37+
const allUp = Object.values(checks).every((v) => v === 'up');
38+
1639
return {
17-
status: 'ok',
18-
checks: {
19-
database: 'up',
20-
},
40+
status: allUp ? 'ok' : 'error',
41+
checks,
2142
timestamp: new Date().toISOString(),
2243
};
2344
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
23
import { HealthController } from './health.controller';
34

45
@Module({
6+
imports: [TypeOrmModule],
57
controllers: [HealthController],
68
})
79
export class HealthModule {}

backend/src/transfers/transfers.controller.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,18 @@ export class TransfersController {
4949
reject(@Param('id') id: string, @Body('reason') reason: string) {
5050
return this.transfersService.reject(id, reason || 'Rejected by manager');
5151
}
52+
53+
@Post(':id/cancel')
54+
@ApiOperation({ summary: 'Cancel a pending asset transfer' })
55+
@ApiResponse({ status: 200, description: 'Transfer cancelled' })
56+
cancel(@Param('id') id: string) {
57+
return this.transfersService.cancel(id);
58+
}
59+
60+
@Post(':id/complete')
61+
@ApiOperation({ summary: 'Mark an approved transfer as completed' })
62+
@ApiResponse({ status: 200, description: 'Transfer completed' })
63+
complete(@Param('id') id: string) {
64+
return this.transfersService.complete(id);
65+
}
5266
}

backend/src/transfers/transfers.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Module } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
33
import { AssetTransfer } from './entities/asset-transfer.entity';
4+
import { Asset } from '../assets/entities/asset.entity';
45
import { TransfersService } from './transfers.service';
56
import { TransfersController } from './transfers.controller';
67

78
@Module({
8-
imports: [TypeOrmModule.forFeature([AssetTransfer])],
9+
imports: [TypeOrmModule.forFeature([AssetTransfer, Asset])],
910
providers: [TransfersService],
1011
controllers: [TransfersController],
1112
exports: [TransfersService],

backend/src/transfers/transfers.service.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import {
99
AssetTransfer,
1010
TransferStatus,
1111
} from './entities/asset-transfer.entity';
12+
import { Asset } from '../assets/entities/asset.entity';
1213

1314
@Injectable()
1415
export class TransfersService {
1516
constructor(
1617
@InjectRepository(AssetTransfer)
1718
private readonly transferRepo: Repository<AssetTransfer>,
19+
@InjectRepository(Asset)
20+
private readonly assetRepo: Repository<Asset>,
1821
) {}
1922

2023
async findAll() {
@@ -40,9 +43,20 @@ export class TransfersService {
4043
if (tr.status !== TransferStatus.PENDING) {
4144
throw new BadRequestException('Only pending transfers can be approved');
4245
}
43-
tr.status = TransferStatus.APPROVED;
44-
tr.approvedByUserId = approverUserId;
45-
return this.transferRepo.save(tr);
46+
47+
await this.transferRepo.manager.transaction(async (manager) => {
48+
tr.status = TransferStatus.APPROVED;
49+
tr.approvedByUserId = approverUserId;
50+
await manager.save(tr);
51+
52+
const asset = await manager.findOneBy(Asset, { id: tr.assetId });
53+
if (asset) {
54+
asset.departmentId = tr.toDepartmentId;
55+
await manager.save(asset);
56+
}
57+
});
58+
59+
return this.findById(id);
4660
}
4761

4862
async reject(id: string, reason: string) {
@@ -54,4 +68,24 @@ export class TransfersService {
5468
tr.rejectionReason = reason;
5569
return this.transferRepo.save(tr);
5670
}
71+
72+
async cancel(id: string) {
73+
const tr = await this.findById(id);
74+
if (tr.status !== TransferStatus.PENDING) {
75+
throw new BadRequestException('Only pending transfers can be cancelled');
76+
}
77+
tr.status = TransferStatus.CANCELLED;
78+
return this.transferRepo.save(tr);
79+
}
80+
81+
async complete(id: string) {
82+
const tr = await this.findById(id);
83+
if (tr.status !== TransferStatus.APPROVED) {
84+
throw new BadRequestException(
85+
'Only approved transfers can be completed',
86+
);
87+
}
88+
tr.status = TransferStatus.COMPLETED;
89+
return this.transferRepo.save(tr);
90+
}
5791
}

0 commit comments

Comments
 (0)