본문으로 건너뛰기

Part 3: API 보안 설계

항목내용
문서명Part 3: API 보안 설계 (API Security Design)
제품명DTA Wide Sleep Management Platform
작성일2026-02-10
적용범위Part 3 (백엔드 API)

1. API 보안 개요

목표: OWASP API Top 10 준수, 안전한 RESTful API 설계

보안 원칙:

  • 인증/인가 필수 (JWT Bearer Token)
  • 입력 검증 (DTO + class-validator)
  • Rate Limiting (Redis 기반)
  • 에러 메시지 최소 정보 노출
  • 관리자 엔드포인트 추가 보호

2. 인증 및 인가 (Authentication & Authorization)

2.1 JWT 기반 인증

AppToken 구조 (RS256 비대칭 서명):

// AppToken Payload (실제 구현 - app-token.guard.ts)
{
"appId": "app-uuid",
"deviceId": "device-uuid",
"jti": "token-unique-id",
"exp": 1707579000, // 30분 후 만료
"iat": 1707577200
}

인증 플로우:

토큰 무효화 정책:

비활성 감지 구현 현황:

항목비고
AccessToken TTL 30분 만료JWT exp 클레임 기반
RefreshToken TTL 14일 만료JWT exp 클레임 기반, 장기 미사용

2.2 역할 기반 인가 (RBAC)

Decorator 사용:

@Controller('sleep')
@UseGuards(AppTokenGuard, FlexibleAuthGuard)
export class SleepController {
@Get('/logs')
@Roles('patient', 'clinician', 'admin')
async getSleepLogs(@CurrentUser() user: TokenPayload) {
// Patient: 본인 데이터만
// Clinician: 할당 환자만
// Admin: 모든 데이터
}

@Delete('/logs/:id')
@Roles('patient', 'admin')
async deleteSleepLog(@Param('id') id: string) {
// Patient: 본인 데이터만 삭제
// Admin: 모든 데이터 삭제 가능
}
}

2.3 앱 요청 무결성 및 진위성 검증

현재 구현된 검증 레이어:

검증 레이어구현 상태참조 파일비고
AppToken (RS256) 앱 진위성 검증✅ 구현됨app-token.guard.ts, app-token.service.tsJWK 기반 RS256 서명 + DB 상태 확인
Device ID SHA-256 해시 검증✅ 구현됨device-identifier.service.ts요청 deviceIdHash vs 서버 계산값 비교
PubSub/Webhook GCP OIDC JWT 검증✅ 구현됨pubsub.guard.ts, webhook-validation.service.tsGCP Pub/Sub 요청 인증
HMAC 기반 요청 본문 서명 검증❌ 미구현-AppToken 이외 추가 서명 없음
응답 본문 HMAC/서명 추가❌ 미구현-응답에 서명 레이어 없음

3. 입력 검증 (Input Validation)

3.1 DTO 기반 검증

CreateSleepLogDto:

import { IsString, IsDateString, IsOptional, IsInt, Min, Max } from 'class-validator';

export class CreateSleepLogDto {
@IsDateString()
bedtime: string; // ISO 8601 형식

@IsDateString()
wakeTime: string;

@IsOptional()
@IsInt()
@Min(0)
@Max(480) // 최대 8시간
sol?: number; // Sleep Onset Latency (분)

@IsOptional()
@IsString()
@MaxLength(500)
notes?: string;
}

ValidationPipe 설정:

// main.ts
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 자동 타입 변환
whitelist: true, // DTO에 없는 속성 제거
forbidNonWhitelisted: true, // 추가 속성 시 에러
exceptionFactory: (errors) => {
// 커스텀 에러 응답
return new BadRequestException({
code: 'VALIDATION_ERROR',
message: 'Input validation failed',
details: errors.map(e => ({
property: e.property,
constraints: e.constraints
}))
});
}
})
);

3.2 SQL Injection 방지

Prisma ORM 파라미터화:

// ✅ SAFE: Prisma 파라미터화된 쿼리
async findByUserId(userId: string): Promise<SleepLog[]> {
return this.prisma.sleepLog.findMany({
where: { userId: userId } // 자동 파라미터화
});
}

3.3 XSS 방지

HTML 이스케이프:

import { sanitize } from 'class-sanitizer';

export class CreateNoteDto {
@IsString()
@MaxLength(1000)
@sanitize() // HTML 태그 제거
content: string;
}

// 또는 수동 이스케이프
function syntax(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}

4. Rate Limiting 및 Anti-Abuse

4.1 Rate Limiting 정책

엔드포인트 유형제한윈도우초과 시
인증 (로그인)5 req/분사용자당429 + 1분 대기
데이터 조회 (GET)100 req/분사용자당429 + 재시도 안내
데이터 생성 (POST)10 req/분사용자당429 + 재시도 안내
관리자 API60 req/분사용자당429 + 로그 기록
Public API1000 req/분IP당429 + CAPTCHA

4.2 Redis 기반 Rate Limiter

구현 (NestJS Interceptor):

@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
constructor(private readonly redis: RedisService) {}

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const userId = request.user?.userId || request.ip;
const key = `rate-limit:${request.path}:${userId}`;

const current = await this.redis.incr(key);
if (current === 1) {
await this.redis.expire(key, 60); // 1분 TTL
}

const limit = this.getLimit(request.path);
if (current > limit) {
throw new TooManyRequestsException(`Rate limit exceeded. Try again in 60 seconds.`);
}

return next.handle();
}
}

4.3 Brute Force 방어

로그인 실패 제한:

async login(email: string, password: string) {
const key = `login-attempts:${email}`;
const attempts = await this.redis.get(key);

if (attempts && parseInt(attempts) >= 5) {
throw new TooManyRequestsException('Too many login attempts. Try again in 15 minutes.');
}

// 비밀번호 검증
const isValid = await this.verifyPassword(email, password);

if (!isValid) {
await this.redis.incr(key);
await this.redis.expire(key, 900); // 15분
throw new UnauthorizedException('Invalid credentials');
}

// 성공 시 카운터 리셋
await this.redis.del(key);
return this.generateToken(user);
}

5. 오류 메시지 정책 (Error Message Policy)

5.1 안전한 에러 응답

프로덕션 환경:

// ✅ SAFE: 최소 정보만 노출
{
"code": 2051,
"message": "INVALID_TOKEN",
"detail": "토큰이 유효하지 않습니다.",
"timestamp": "2026-02-10T12:00:00.000Z"
}

예외 필터 (Global Exception Filter):

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();

let status = 500;
let code = 'INTERNAL_SERVER_ERROR';
let detail = '서버 내부 오류가 발생했습니다.';

if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
code = exceptionResponse['code'] || exception.message;
detail = exceptionResponse['detail'] || exception.message;
}

// ❌ 스택 트레이스 절대 노출 금지
// ❌ 데이터베이스 연결 정보 노출 금지
// ❌ 파일 경로 노출 금지

response.status(status).json({
code,
message: code,
detail,
timestamp: new Date().toISOString()
});

// 내부 로깅 (Cloud Logging / LoggerService)
if (status >= 500) {
this.logger.error(exception);
}
}
}
항목구현 상태비고
에러 메시지 최소화 (스택 트레이스 미노출)✅ 구현됨GlobalExceptionFilter

5.2 HTTP 상태 코드 매핑

시나리오HTTP 상태응답 코드메시지
인증 실패 (토큰 없음)401AUTH_REQUIRED"인증이 필요합니다."
인증 실패 (토큰 만료)401TOKEN_EXPIRED"토큰이 만료되었습니다."
권한 없음403FORBIDDEN"접근 권한이 없습니다."
리소스 없음404NOT_FOUND"리소스를 찾을 수 없습니다."
입력 검증 실패400VALIDATION_ERROR"입력값이 올바르지 않습니다."
Rate Limit 초과429RATE_LIMIT_EXCEEDED"요청 한도를 초과했습니다."
서버 오류500INTERNAL_ERROR"서버 오류가 발생했습니다."

6. 관리자 엔드포인트 보호 (Admin Endpoint Protection)

6.1 추가 보안 계층

관리자 전용 Guard:

@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;

// 1. Admin 역할 확인
if (!user.roles?.includes('admin')) {
return false;
}

// 2. IP 화이트리스트 확인 (선택적)
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!this.isAllowedIp(clientIp)) {
this.logger.warn(`Admin access from unauthorized IP: ${clientIp}`);
return false;
}

// 3. 시간 기반 접근 제한 (선택적)
const hour = new Date().getUTCHours();
if (hour >= 0 && hour < 6) {
this.logger.warn(`Admin access during restricted hours: ${user.userId}`);
// 경고만, 차단하지 않음
}

return true;
}
}

6.2 민감 API 엔드포인트 목록

엔드포인트역할추가 보안감사 로그
DELETE /v1/admin/users/:idAdminMFA 재확인
POST /v1/admin/users/:id/rolesAdminJira 승인
GET /v1/admin/audit-logsAdminIP 화이트리스트
POST /v1/admin/system/settingsAdminJira 승인
GET /v1/admin/users/:id/data-exportAdmin, CS데이터 익명화

7. API 보안 체크리스트

#항목구현검증
1모든 엔드포인트 인증 필수 (public 제외)API Test
2역할 기반 인가 (RBAC)API Test
3DTO 입력 검증 (class-validator)API Test
4SQL Injection 방지 (Prisma ORM)SAST
5XSS 방지 (HTML 이스케이프)DAST
6CSRF 방지 (SameSite Cookie)N/AJWT Bearer Token 사용
7Rate Limiting (Redis)API Test
8안전한 에러 메시지API Test
9HTTPS 강제 (TLS 1.3)GCP SSL
10민감 데이터 로깅 금지로그 샘플 검증
11CORS 적절한 설정설정 파일 검토
12API 버저닝 (v1/)URL 패턴
13관리자 API 추가 보호Admin Guard
14감사 로그 (민감 행위)로그 샘플

8. OWASP API Top 10 준수

#OWASP API Top 10구현 통제상태
1Broken Object Level AuthorizationuserId 검증, RBAC
2Broken AuthenticationJWT, Rate Limiting
3Broken Object Property Level AuthorizationDTO Whitelist
4Unrestricted Resource ConsumptionRate Limiting, 페이지네이션
5Broken Function Level AuthorizationRoles Guard
6Unrestricted Access to Sensitive Business FlowsRate Limiting, Block Access
7Server Side Request Forgery (SSRF)URL 검증, 화이트리스트
8Security Misconfiguration안전한 기본 설정, SAST
9Improper Inventory ManagementSwagger 문서, API 버저닝
10Unsafe Consumption of APIs외부 API TLS 검증, 타임아웃

9. Swagger API 문서 보안

9.1 Swagger UI 접근 제한

프로덕션 환경:

// main.ts
if (process.env.NODE_ENV !== 'production') {
// 스테이징/개발 환경만 Swagger 활성화
const config = new DocumentBuilder()
.setTitle('DTA Wide API')
.setVersion('1.0')
.addBearerAuth()
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('v1/docs', app, document);
} else {
// 프로덕션: Swagger 비활성화 또는 IP 화이트리스트
// [TODO: 프로덕션 Swagger 비활성화 확인 필요]
}

9.2 민감 정보 숨기기

@ApiProperty({
example: 'user@example.com',
description: '사용자 이메일'
// ❌ 절대 포함 금지: 실제 이메일, 토큰, 비밀번호
})
email: string;

@ApiProperty({
example: '********', // 가려진 예시
writeOnly: true, // 응답에 포함 안 함
})
@Exclude() // class-transformer로 자동 제외
password: string;

증빙 및 참조(Artifacts)

  1. API 보안 체크리스트 (본 문서 Section 7)
  2. DTO 검증 코드 - apps/dta-wide-api/src/app/*/dto/*.dto.ts
  3. Guards 구현 - guards/app-token.guard.ts, guards/flexible-auth.guard.ts, guards/webhook.guard.ts
  4. Rate Limiter 코드 - libs/core/redis/src/lib/rate-limit.interceptor.ts
  5. Exception Filter 코드 - apps/dta-wide-api/src/app/filters/global-exception.filter.ts
  6. OWASP ZAP DAST 보고서 - reports/api-dast-latest.pdf
  7. Swagger API 문서 - https://staging-api.dta-wide.com/v1/docs (스테이징)
  8. API 보안 테스트 결과 - test-results/api-security-tests.log
  9. Rate Limiting 테스트 - k6 스크립트 + 결과
  10. 감사 로그 샘플 (관리자 API) - logs/admin-api-calls.log
테스트 항목도구결과비고
SQL InjectionOWASP ZAP[TODO: 실행 필요]Prisma ORM (파라미터화)
XSS (Reflected)OWASP ZAP[TODO: 실행 필요]HTML 이스케이프
Broken AuthenticationBurp Suite[TODO: 실행 필요]AppToken RS256 + Rate Limit
IDOR (Insecure Direct Object Reference)Manual Test[TODO: 실행 필요]userId 검증
Rate Limitingk6 Load Test[TODO: 실행 필요]사용자당 100 req/min
CSRFOWASP ZAPN/AJWT Bearer Token (Cookie 미사용)
민감 정보 노출Manual Review[TODO: 실행 필요]에러 메시지 최소화