Skip to content

fix: 보안 취약점 5건 수정 (Path Traversal, 암호화 키, S3 평문, SQL Injection, 입력 검증)#13

Merged
clroot merged 10 commits into
mainfrom
fix/security-hardening
Feb 26, 2026
Merged

fix: 보안 취약점 5건 수정 (Path Traversal, 암호화 키, S3 평문, SQL Injection, 입력 검증)#13
clroot merged 10 commits into
mainfrom
fix/security-hardening

Conversation

@clroot
Copy link
Copy Markdown
Owner

@clroot clroot commented Feb 26, 2026

Summary

보안 코드리뷰에서 발견된 5건의 보안 취약점을 수정합니다.

# Severity 이슈 해결
1 Critical Path Traversal LocalStorageAdapter.safePath() 경로 경계 검증 + Datasource 이름에 경로 문자 금지
2 Critical 기본 암호화 키 사용 운영 환경에서 키 미설정 시 시작 실패 + 최소 16바이트 검증
3 Critical S3 자격증명 평문 저장 ENC: 접두사 암호화 + 시작 시 기존 평문 자동 마이그레이션
4 Major SQL Injection (orderBy) regex 화이트리스트로 식별자 검증
5 Major 입력 검증 부재 Jakarta Validation + 전체 Controller @Valid + 에러 핸들러

Changes

Path Traversal 방어

  • LocalStorageAdapter: 모든 public 메서드에서 safePath() 경로 경계 검증
  • Datasource.create() / DatasourceService.update(): 이름에 /, \, .. 포함 시 reject

기본 암호화 키 제거

  • AesEncryptionAdapter: dev 프로필에서만 기본 키 허용 (경고 로그), 그 외 IllegalStateException
  • 최소 키 길이 16바이트 검증 추가

S3 자격증명 암호화

  • StorageConfigMapper: 저장 시 ENC: + 암호화, 읽기 시 자동 복호화 (레거시 평문 호환)
  • StorageConfigMigrationRunner: 시작 시 기존 평문 자격증명 자동 암호화

SQL Injection 방어

  • DuckDbQueryEngine.sanitizeOrderBy(): identifier (ASC|DESC)? 패턴만 허용, 식별자 이스케이프

입력 검증

  • 6개 DTO에 Jakarta Validation 어노테이션 추가
  • 모든 Controller @RequestBody@Valid 추가
  • GlobalExceptionHandlerMethodArgumentNotValidException 핸들러 추가

Test plan

  • LocalStorageAdapterTest — path traversal 차단 (9 cases)
  • DatasourceTest — 이름 경로 문자 검증 (4 cases)
  • DatasourceServiceTest — update 경로 이름 검증
  • AesEncryptionAdapterTest — 프로필별 동작 + 키 길이 (5 cases)
  • StorageConfigMapperTest — 암복호화 + 레거시 호환 (5 cases)
  • StorageConfigMigrationRunnerTest — 마이그레이션 시나리오 (4 cases)
  • DuckDbQueryEngineTest — orderBy 허용/차단 (4 cases)
  • 전체 테스트 통과 (./gradlew test)

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @clroot, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 코드 보안 리뷰에서 발견된 5가지 주요 보안 취약점(경로 탐색, 기본 암호화 키 사용, S3 자격 증명 평문 저장, SQL Injection, 입력 유효성 검증 부재)을 해결하여 시스템의 전반적인 보안 수준과 견고성을 크게 향상시킵니다. 사용자 입력과 내부 데이터 처리 과정에서 발생할 수 있는 잠재적인 공격 벡터를 차단하고, 민감한 정보의 안전한 저장을 보장함으로써 애플리케이션의 신뢰성을 강화하는 데 중점을 두었습니다.

Highlights

  • 경로 탐색 (Path Traversal) 방어 강화: LocalStorageAdapter의 모든 공개 메서드에 safePath()를 적용하여 경로 경계 검증을 수행하고, Datasource 이름에 경로 문자를 포함할 수 없도록 유효성 검사를 추가했습니다.
  • 기본 암호화 키 사용 제한: 운영 환경에서 암호화 키가 설정되지 않은 경우 애플리케이션 시작을 실패하도록 변경하고, 최소 16바이트 길이의 키를 사용하도록 검증 로직을 추가했습니다. 개발 환경에서는 기본 키 사용 시 경고 로그를 남깁니다.
  • S3 자격 증명 암호화: S3 자격 증명을 저장 시 ENC: 접두사를 붙여 암호화하고, 읽을 때는 자동으로 복호화하도록 StorageConfigMapper를 수정했습니다. 또한, 애플리케이션 시작 시 기존의 평문 자격 증명을 자동으로 암호화하는 마이그레이션 로직을 추가했습니다.
  • SQL Injection 방어 (orderBy): DuckDbQueryEngine의 orderBy 절에 대해 정규식을 이용한 화이트리스트 기반 식별자 검증 및 이스케이프 처리를 적용하여 SQL Injection 공격을 방어합니다.
  • 입력 유효성 검증 도입: 6개 DTO에 Jakarta Validation 어노테이션을 추가하고, 모든 @RequestBody를 사용하는 Controller 메서드에 @Valid 어노테이션을 적용했습니다. 또한, MethodArgumentNotValidException을 처리하는 전역 예외 핸들러를 추가하여 유효성 검증 실패 시 일관된 응답을 제공합니다.
Changelog
  • build.gradle.kts
    • 테스트 환경에서 암호화 키를 설정하도록 systemProperty를 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/AuthController.kt
    • @Valid 어노테이션을 추가하여 로그인 및 비밀번호 변경 요청의 입력 유효성 검증을 활성화했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/CompareController.kt
    • @Valid 어노테이션을 추가하여 비교 관련 요청의 입력 유효성 검증을 활성화했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/DatasourceController.kt
    • @Valid 어노테이션을 추가하여 데이터소스 등록 및 업데이트 요청의 입력 유효성 검증을 활성화했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/GlobalExceptionHandler.kt
    • MethodArgumentNotValidException을 처리하는 핸들러를 추가하여 유효성 검증 실패 시 상세한 오류 응답을 제공합니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/QueryController.kt
    • @Valid 어노테이션을 추가하여 쿼리 실행 요청의 입력 유효성 검증을 활성화했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/SetupController.kt
    • @Valid 어노테이션을 추가하여 시스템 초기화 및 스토리지 테스트 요청의 입력 유효성 검증을 활성화했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/SnapshotController.kt
    • @Valid 어노테이션을 추가하여 스냅샷 메타데이터 업데이트 요청의 입력 유효성 검증을 활성화했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/StorageController.kt
    • @Valid 어노테이션을 추가하여 스토리지 설정 업데이트 요청의 입력 유효성 검증을 활성화했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/dto/AuthDtos.kt
    • LoginRequest 및 ChangePasswordRequest DTO 필드에 @notblank@SiZe 유효성 검증 어노테이션을 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/dto/CompareDtos.kt
    • 비교 관련 DTO 필드에 @notblank@notempty 유효성 검증 어노테이션을 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/dto/DatasourceDtos.kt
    • RegisterDatasourceRequest 및 UpdateDatasourceRequest DTO 필드에 @notblank, @SiZe, @min, @max, @notempty 유효성 검증 어노테이션을 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/dto/QueryDtos.kt
    • ExecuteQueryRequest DTO 필드에 @notblank, @min, @max 유효성 검증 어노테이션을 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/dto/SetupDtos.kt
    • InitializeRequest 및 TestStorageRequest DTO 필드에 @notblank@SiZe 유효성 검증 어노테이션을 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/inbound/web/dto/StorageDtos.kt
    • UpdateStorageSettingsRequest DTO 필드에 @notblank 유효성 검증 어노테이션을 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/outbound/persistence/adapter/AesEncryptionAdapter.kt
    • 암호화 키가 설정되지 않은 경우 개발 프로필에서만 기본 키를 허용하고, 그 외에는 IllegalStateException을 발생시키도록 로직을 변경했으며, 최소 키 길이 16바이트를 검증하도록 했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/outbound/persistence/entity/StorageConfigEntity.kt
    • S3 접근 키와 비밀 키 필드를 val에서 var로 변경하여 마이그레이션 중 값을 수정할 수 있도록 했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/outbound/persistence/mapper/EntityMappers.kt
    • StorageConfigMapper에 EncryptionPort를 주입받아 S3 자격 증명을 저장 시 암호화하고 읽을 때 복호화하는 로직을 추가했습니다. ENC: 접두사를 사용하여 암호화 여부를 식별합니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/outbound/persistence/migration/StorageConfigMigrationRunner.kt
    • 애플리케이션 시작 시 기존에 평문으로 저장된 S3 자격 증명을 암호화된 형식으로 자동 마이그레이션하는 ApplicationRunner를 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/outbound/query/DuckDbQueryEngine.kt
    • sanitizeOrderBy 메서드를 추가하여 ORDER BY 절의 SQL Injection을 방어하고, previewTable에서 이를 사용하도록 변경했습니다.
  • src/main/kotlin/io/clroot/snaplake/adapter/outbound/storage/LocalStorageAdapter.kt
    • safePath 메서드를 도입하여 모든 파일 시스템 접근 메서드에서 경로 탐색 공격을 방어하도록 수정했습니다.
  • src/main/kotlin/io/clroot/snaplake/application/service/DatasourceService.kt
    • 데이터소스 이름에 경로 문자가 포함되지 않도록 유효성 검증 로직을 추가했습니다.
  • src/main/kotlin/io/clroot/snaplake/domain/model/Datasource.kt
    • 데이터소스 생성 시 이름에 경로 문자가 포함되지 않도록 유효성 검증 로직을 추가했습니다.
  • src/test/kotlin/io/clroot/snaplake/adapter/outbound/persistence/adapter/AesEncryptionAdapterTest.kt
    • AesEncryptionAdapter의 암호화/복호화 기능, 키 길이 검증, 프로필별 동작에 대한 단위 테스트를 추가했습니다.
  • src/test/kotlin/io/clroot/snaplake/adapter/outbound/persistence/mapper/StorageConfigMapperTest.kt
    • StorageConfigMapper의 S3 자격 증명 암호화/복호화 및 레거시 평문 호환성에 대한 단위 테스트를 추가했습니다.
  • src/test/kotlin/io/clroot/snaplake/adapter/outbound/persistence/migration/StorageConfigMigrationRunnerTest.kt
    • StorageConfigMigrationRunner의 S3 자격 증명 마이그레이션 로직에 대한 단위 테스트를 추가했습니다.
  • src/test/kotlin/io/clroot/snaplake/adapter/outbound/query/DuckDbQueryEngineTest.kt
    • DuckDbQueryEngine의 orderBy 절 SQL Injection 방어 로직에 대한 테스트 케이스를 추가했습니다.
  • src/test/kotlin/io/clroot/snaplake/adapter/outbound/storage/LocalStorageAdapterTest.kt
    • LocalStorageAdapter의 경로 탐색 방어 기능에 대한 다양한 테스트 케이스를 추가했습니다.
  • src/test/kotlin/io/clroot/snaplake/application/service/DatasourceServiceTest.kt
    • DatasourceService의 데이터소스 이름 유효성 검증에 대한 테스트 케이스를 추가했습니다.
  • src/test/kotlin/io/clroot/snaplake/domain/model/DatasourceTest.kt
    • Datasource 모델의 데이터소스 이름 유효성 검증에 대한 테스트 케이스를 추가했습니다.
Activity
  • 작성자가 PR 설명에 명시된 모든 테스트 계획(LocalStorageAdapterTest, DatasourceTest, DatasourceServiceTest, AesEncryptionAdapterTest, StorageConfigMapperTest, StorageConfigMigrationRunnerTest, DuckDbQueryEngineTest, 전체 테스트 통과)을 완료했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@clroot clroot merged commit 7853cd4 into main Feb 26, 2026
@clroot clroot deleted the fix/security-hardening branch February 26, 2026 11:33
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

전반적으로 훌륭한 수정입니다. 5가지 보안 취약점을 해결하기 위해 체계적으로 접근했으며, 각 수정 사항에 대해 철저한 테스트를 추가한 점이 인상적입니다. 코드의 안정성과 보안성이 크게 향상되었습니다. 코드 가독성 및 성능 개선을 위한 몇 가지 제안 사항을 남겼으니 확인해 보시기 바랍니다.

Comment on lines +17 to +28
var updated = false

if (entity.s3AccessKey != null && !entity.s3AccessKey!!.startsWith(ENCRYPTED_PREFIX)) {
entity.s3AccessKey = ENCRYPTED_PREFIX + encryptionPort.encrypt(entity.s3AccessKey!!)
updated = true
}
if (entity.s3SecretKey != null && !entity.s3SecretKey!!.startsWith(ENCRYPTED_PREFIX)) {
entity.s3SecretKey = ENCRYPTED_PREFIX + encryptionPort.encrypt(entity.s3SecretKey!!)
updated = true
}

if (updated) repository.save(entity)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

현재 코드는 s3AccessKeys3SecretKey에 대해 거의 동일한 로직을 반복하고 있으며, !! (non-null assertion) 연산자를 사용하고 있습니다.
Kotlin의 lettakeIf를 사용하여 코드를 더 간결하고 안전하게 개선할 수 있습니다. 이렇게 하면 중복을 줄이고 !! 사용을 피하여 코드 가독성과 안정성을 높일 수 있습니다.

Suggested change
var updated = false
if (entity.s3AccessKey != null && !entity.s3AccessKey!!.startsWith(ENCRYPTED_PREFIX)) {
entity.s3AccessKey = ENCRYPTED_PREFIX + encryptionPort.encrypt(entity.s3AccessKey!!)
updated = true
}
if (entity.s3SecretKey != null && !entity.s3SecretKey!!.startsWith(ENCRYPTED_PREFIX)) {
entity.s3SecretKey = ENCRYPTED_PREFIX + encryptionPort.encrypt(entity.s3SecretKey!!)
updated = true
}
if (updated) repository.save(entity)
var needsSave = false
entity.s3AccessKey?.takeIf { !it.startsWith(ENCRYPTED_PREFIX) }?.let {
entity.s3AccessKey = ENCRYPTED_PREFIX + encryptionPort.encrypt(it)
needsSave = true
}
entity.s3SecretKey?.takeIf { !it.startsWith(ENCRYPTED_PREFIX) }?.let {
entity.s3SecretKey = ENCRYPTED_PREFIX + encryptionPort.encrypt(it)
needsSave = true
}
if (needsSave) repository.save(entity)

Comment on lines +161 to +165
val pattern =
Regex(
"""^\s*"([a-zA-Z_][a-zA-Z0-9_ ]*)"\s*(ASC|DESC)?\s*$|^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(ASC|DESC)?\s*$""",
RegexOption.IGNORE_CASE,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

sanitizeOrderBy 함수가 호출될 때마다 Regex 객체를 생성하는 것은 비효율적일 수 있습니다. 이 정규식을 클래스의 companion object에 상수로 추출하여, 클래스 로딩 시 한 번만 컴파일되도록 하는 것이 좋습니다. 이렇게 하면 성능이 개선되고 코드가 더 명확해집니다.

아래와 같이 수정할 수 있습니다:

@Component
class DuckDbQueryEngine : QueryEngine {
    companion object {
        private val ORDER_BY_PATTERN = Regex(
            """^\s*"([a-zA-Z_][a-zA-Z0-9_ ]*)"\s*(ASC|DESC)?\s*$|^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(ASC|DESC)?\s*$""",
            RegexOption.IGNORE_CASE,
        )
    }
    
    // ...

    private fun sanitizeOrderBy(orderBy: String): String {
        return orderBy.split(",").map { part ->
            val match =
                ORDER_BY_PATTERN.matchEntire(part.trim())
                    ?: throw IllegalArgumentException("Invalid ORDER BY clause: ${part.trim()}")
            // ... (rest of the function)
        }.joinToString(", ")
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant