Windows 앱 개발 보안 최소 체크리스트

· 업데이트: · · Windows 개발, 보안, 설계, C# / .NET, Win32

수정 이력(7건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635132)
권한 분리 구도, 서명과 타임스탬프, 업데이트 경로 검증, DPAPI의 저장·읽기 왕복, 인증서 핀닝 판정 흐름 등을 그림으로도 따라갈 수 있도록 Mermaid 그림을 21개 추가했습니다(본문 500~750자당 1그림 규약에 맞춘 것입니다). 본문 문장은 바꾸지 않았습니다.
글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건) 대응으로 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
인증서를 핀닝하는 예에서, 지문이 일치하면 `SslPolicyErrors` 내용과 관계없이 `true`를 반환하던 것을 고쳤습니다. 이 방식에서는 핀닝한 인증서가 만료되어도, 키가 유출되어 해지한 뒤에도 계속 통과합니다. 인정하는 것은 `RemoteCertificateChainErrors`뿐으로 하고, 체인 상태도 `UntrustedRoot`와 `PartialChain`(사내 CA가 단말에 없음) 이외는 거부하도록 했습니다. `CheckCertificateRevocationList = true`를 추가하고, 해지 확인 수단이 없을 때 어떻게 메울지도 본문에 적었습니다.
각 항목의 확인 방법 절을 새로 만들었습니다(매니페스트 확인, `Get-AuthenticodeSignature`, 비밀 정보나 SQL 연결 검색어, Process Monitor 필터 조건, `dotnet list package --vulnerable`). DPAPI 예를 저장과 읽기 왕복으로 바꾸고, entropy 일치 요건과 `CurrentUser`와 `LocalMachine`의 차이를 추가했습니다. .NET 9 이후는 `ServicePointManager` 콜백이 `SocketsHttpHandler`에 매핑된다는 점을 근거로, 핸들러 단위로 대상을 한정하는 예를 추가했습니다.
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635131)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

Go Komura (2026). 「Windows 앱 개발 보안 최소 체크리스트」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635131 https://comcomponent.com/ko/blog/windows-app-security-minimum-checklist/

DOI(최신 버전)
10.5281/zenodo.21635131
DOI(이 버전)
10.5281/zenodo.22217433

Excel판 체크리스트 다운로드

이 파일의 내용은 4장의 릴리스 전 체크리스트(8개 카테고리 32개 항목)와 같습니다. 다른 점은 StatusNotes 입력란이 있다는 것과, Checklist-ja / Checklist-en 두 시트에 일본어와 영어가 모두 들어 있다는 것뿐입니다. 읽으면서 확인할 때는 4장, 리뷰 기록으로 배포할 때는 Excel판, 이렇게 나누면 충분합니다.

Windows 앱 보안이라고 하면 이야기가 갑자기 커지기 쉽습니다. 제로 트러스트, EDR, SBOM(Software Bill of Materials, 소프트웨어 구성 요소 목록), 인증서 운영, 취약성 관리. 모두 중요하지만, 실무에서는 그 전에 빠뜨리고 싶지 않은 기본이 꽤 있습니다. 특히 다음과 같은 앱에서는 「고도 방어」보다 먼저 기본의 빈틈을 막는 편이 효과가 큽니다.

  • WPF / WinForms / WinUI 데스크톱 앱
  • C++ / C# Win32 앱
  • 장치 연동, 파일 연동, DB 연결, 사내 배포 도구
  • 자동 업데이트 메커니즘을 가진 업무 앱
  • Windows 서비스나 보조 EXE를 포함하는 구성

Windows 앱 개발에서는 전부를 한 번에 완벽하게 하기보다, 우선 명백히 위험한 구멍을 남기지 않는 편이 현실적입니다. 여기서는 설계, 구현, 배포, 운영의 순서로 최소한 빠뜨리고 싶지 않은 포인트를 확인하기 쉬운 형태로 정리합니다.

이 글의 진행 방식고도 방어보다 먼저 기본의 빈틈을 막는다는 방침으로, 설계·구현·배포·운영의 순서로 최소한의 포인트를 정리한다는 글의 흐름을 나타내는 그림.그 전에고도 방어(제로 트러스트 등)기본의 빈틈을 막기설계구현배포운영

그림1: 고도 방어 전에 기본의 빈틈을 막는 방침으로, 설계·구현·배포·운영의 순서로 살펴봅니다.

1. 먼저 결론

  • 처음에 빠뜨리고 싶지 않은 것은 불필요한 관리자 권한을 요구하지 않는 것, 서명하는 것, 비밀 정보를 평문으로 두지 않는 것, 인증서 검증을 무효화하지 않는 것입니다.
  • Windows 앱은 배포물 자체가 공격 표면이 됩니다. EXE / DLL / MSI / MSIX / 자동 업데이트 모듈까지 포함해 보는 편이 안전합니다.
  • ServerCertificateValidationCallback => true, 평문 연결 문자열, LoadLibrary("foo.dll")의 느슨한 로드, 문자열 연결로 SQL을 실행하는 것은 최소한의 기준에서도 피해야 할 항목입니다.
  • 관리자 권한이 필요한 처리가 일부뿐이라면, 앱 전체를 권한 상승시키는 것이 아니라 그 부분만 별도 EXE나 service로 나누는 편이 안전합니다.
  • Windows에서 배포하는 앱은 서명 + 타임스탬프를 전제로 생각하는 편이 좋습니다. 사용자에 대한 신뢰뿐 아니라 변조 탐지나 운영 설명도 쉬워집니다.
  • 저장 시 기밀 정보는 용도에 따라 DPAPI / ProtectedDataCredential Locker를 구분해 사용합니다. 적어도 appsettings.json에 평문으로 두는 상태는 벗어나고 싶습니다.
  • 로그는 많으면 좋은 것이 아닙니다. 토큰, 비밀번호, 연결 문자열, 개인 정보, 전체 요청 본문을 그대로 남기면 로그 자체가 사고의 주역이 됩니다.

최소한의 보안은 특수한 기능을 더하는 것보다 위험한 기본 동작이나 허술한 구현을 남기지 않는 것입니다.

최소한의 보안에 대한 생각최소한의 보안은 특수한 기능을 더하는 것이 아니라, 위험한 기본 동작이나 허술한 구현을 남기지 않는 것이라는 정리를 나타내는 그림.최소한의 주안점이 아님이것이 주안점특수한 기능을 더하기최소한의 보안위험한 기본 동작·허술한 구현을 남기지 않기

그림2: 최소한의 기준은 기능 추가가 아니라, 위험한 기본 동작이나 허술한 구현을 남기지 않는 것입니다.

이 글의 지식 맵

이 글은 WPF/WinForms/WinUI/C++/C#의 Windows 업무 앱을 대상으로, 릴리스 전에 빠뜨리고 싶지 않은 최소한의 보안 대책을 체크리스트로 정리하고 있습니다. 중심이 되는 것은 앱 전체를 requireAdministrator로 하지 않고 필요한 처리만 별도 프로세스나 서비스로 분리하는 것, 배포물에 코드 서명과 타임스탬프를 붙여 미서명 배포나 인증서 검증의 상시 스킵을 피하는 것, 비밀 정보를 평문 설정에 두지 않고 DPAPI 등으로 보호하는 것입니다. 아울러 SQL의 문자열 연결이 초래하는 SQL 인젝션을 파라미터화로 방지하는 것, DLL을 이름만으로 읽으면 검색 순서 하이재킹을 부르는 것, 로그에 기밀 정보를 남기면 로그 자체가 유출 경로가 되는 것도 다루고 있습니다.

Windows 앱 보안 최소 체크리스트의 지식 맵관리자 권한 처리, 코드 서명과 업데이트물 검증, 비밀 정보 보호, SQL 인젝션이나 DLL 읽기 같은 입력·실행 시 위험, 로그를 통한 기밀 유출이라는 최소한의 보안 항목들 사이의 관계를 보여주는 그림에서 구성할 수 있다에서 구성할 수 있다사용은 비권장권장되는 대응원인이 될 수 있다원인이 될 수 있다방지한다완화한다방지한다권장되는 대응권장되는 대응전제로 한다사용은 비권장원인이 될 수 있다방지한다원인이 될 수 있다권장되는 대응전제로 한다이용한다전제로 한다권장되는 대응관리자 권한코드 서명 인증서앱 비밀 정보asInvoker 실행 수준requireAdministrator 실행 수준대화형 사용자의 데스크톱 앱관리자 권한 처리 분리DLL 검색 순서 하이재킹DLL 이름만으로 로드서명되지 않은 바이너리코드 서명 타임스탬프인증서 만료업데이트 서명·해시 검증검증되지 않은 업데이트 적용인증서 피닝인증서 검증 상시 우회인증서 폐기 확인SQL 문자열 연결SQL 인젝션플레이스홀더(프리페어드 스테이트먼트)로그에 기밀 정보 출력로그를 통한 기밀 정보 노출DPAPI비밀 정보의 평문 저장Windows 서비스

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 21건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 이 글의 대상과 「최소한」의 의미

2.1. 대상으로 하는 범위

이 글에서 가정하는 것은 다음과 같은 Windows 앱입니다.

  • WPF / WinForms / WinUI 데스크톱 앱
  • C++ / C# Win32 앱
  • 사내 배포 도구, 장치 연동 도구, 감시 도구
  • 보조 EXE, Windows 서비스, 업데이터를 포함하는 구성
  • EXE / MSI / MSIX로 배포하는 업무용 소프트웨어

여기서 말하는 「최소한」은 감사를 통과하는 최종 형태가 아니라, 이것이 빠져 있으면 평범하게 사고로 이어지는 항목입니다.

이 글에서 「최소한」의 의미이 글의 최소한은 감사를 통과하는 최종 형태가 아니라, 빠져 있으면 평범하게 사고로 이어지는 항목을 가리킨다는 구분을 나타내는 그림.여기서는 가리키지 않음이것을 가리킴감사를 통과하는 최종 형태이 글의 「최소한」빠져 있으면 평범하게 사고로 이어지는 항목

그림3: 「최소한」이 가리키는 것은 감사 대응의 최종 형태가 아니라, 빠지면 평범하게 사고로 이어지는 항목 쪽입니다.

코드 예의 전제도 맞춰 둡니다. C# 예는 .NET 8 이후, C++ 예는 Win32 API를 가정합니다. .NET Framework 4.8에서도 생각은 그대로이지만, 권장하는 작성 방식이 바뀐 곳이 있습니다. 대표 예가 3.6의 ServicePointManager 주변으로, 신규 코드에서는 IHttpClientFactoryHttpClient를 쓰는 전제로 바뀌었습니다. 이전 세대 코드를 다시 읽을 때는 그 점만 주의해 주십시오.

2.2. 대상 외

한편, 이 글의 중심에서는 빼는 것도 있습니다.

  • 기업 전체의 제로 트러스트 설계
  • EDR / SIEM / DLP / MDM의 전체 운영
  • 커널 드라이버의 상세한 하드닝
  • 암호 설계 자체를 처음부터 하는 이야기
  • 고도 위협 분석이나 포렌식 절차

즉, 「조직 전체의 거대한 보안 대책」이 아니라 Windows 앱 개발자가 릴리스 전에 스스로 빠뜨리기 어려운 기본 기준을 다룹니다.

대상과 대상 외의 구분조직 전체의 거대한 보안 대책은 대상 외로 두고, Windows 앱 개발자가 릴리스 전에 스스로 빠뜨리기 어려운 기본 기준을 대상으로 한다는 범위의 구분을 나타내는 그림.이 글에서는 다루지 않음이것을 다룸조직 전체의 거대한 대책이 글의 범위개발자가 스스로 빠뜨리기 어려운 기본 기준

그림4: 다루는 것은 조직 전체의 대책이 아니라, 개발자가 릴리스 전에 스스로 지킬 수 있는 기본 기준입니다.

3. 먼저 보는 체크리스트

세세한 논의 전에, 우선 전체를 조망할 수 있는 표를 둡니다. 여기만 봐도 다시 볼 위치의 감은 잡힙니다.

3.1. 전체 모습

확인할 항목 최소한 할 일 전형적인 NG
실행 권한 asInvoker를 기본으로 하고, 권한 상승이 필요한 처리만 분리한다 앱 전체를 requireAdministrator로 한다
배포물의 신뢰성 EXE / DLL / MSI / MSIX에 코드 서명하고, 타임스탬프도 붙인다 서명하지 않은 채로 배포한다
업데이트 업데이트 소스를 고정하고, HTTPS와 서명 확인으로 변조를 탐지한다 HTTP로 받은 뒤 그대로 덮어쓴다
기밀 정보 소스 코드나 평문 설정에 비밀을 두지 않고, DPAPI / Credential Locker 등을 쓴다 API 키나 연결 문자열을 설정 파일에 평문으로 둔다
통신 HTTPS를 쓰고, 인증서 검증을 무효화하지 않는다 return true로 인증서 검증을 항상 건너뛴다
외부 입력 SQL, 파일, IPC, URI, CSV, JSON 등을 모두 검증한다 「사내 도구니까」라며 그대로 통과시킨다
DLL 로드 절대 경로, SetDefaultDllDirectories, 안전한 검색 순서를 쓴다 LoadLibrary("foo.dll")를 현재 디렉터리에 맡긴다
로그 토큰, 비밀번호, PII를 마스킹하고, 사용자용 오류는 구분해 낸다 예외 상세나 연결 문자열을 그대로 표시·저장한다
의존 관계 SDK, NuGet, VC++ 런타임, OSS 의존을 지속적으로 업데이트한다 수년 단위로 고정하고, 취약성 정보도 추적하지 않는다

3.2. 권한은 asInvoker를 기본으로 한다

Windows 앱에서 가장 먼저 다시 보고 싶은 곳이 여기입니다. 앱 전체를 관리자 권한으로 실행하면 버그, DLL 바꿔 치기, 설정 파일 오독, 외부 입력의 허점이 그대로 강한 권한으로 실행됩니다.

앱 전체를 관리자 권한으로 둔 경우의 위험앱 전체를 관리자 권한으로 실행하면 버그, DLL 바꿔 치기, 설정 파일 오독, 외부 입력의 허점이 그대로 강한 권한으로 실행된다는 것을 나타내는 그림.앱 전체를 관리자 권한으로 실행버그DLL 바꿔 치기설정 오독·입력 허점그대로 강한 권한으로 실행된다

그림5: 앱 전체를 권한 상승시키면, 안고 있는 허점 전부가 강한 권한으로 실행됩니다.

기본 방침은 다음과 같습니다.

  • 일반 UI 앱은 asInvoker
  • 관리자 권한이 필요한 처리만 별도 프로세스나 service로 분리한다
  • 권한 상승은 필요한 순간에만 한다
  • 보조 EXE나 service에 넘기는 입력도 검증한다

평소에는 열람과 편집만 하는 데스크톱 앱에서, 설치나 방화벽 설정 변경만 관리자 권한이 필요하다면, 앱 전체를 requireAdministrator로 두기보다 권한 상승이 필요한 부분만 broker로 모으는 편이 안전합니다.

<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  <security>
    <requestedPrivileges>
      <requestedExecutionLevel level="asInvoker" uiAccess="false" />
    </requestedPrivileges>
  </security>
</trustInfo>

「관리자로 돌리면 편하다」는 대개 나중에 영향을 줍니다. 최소 권한으로 실행하고, 그래도 필요한 조작만 잘라 내는 편이 사고 영향 범위는 꽤 작아집니다.

권한 상승이 필요한 처리만 분리하는 구도일반 UI 앱은 asInvoker로 실행하고, 관리자 권한이 필요한 처리만 별도 프로세스나 service의 broker로 모아, 권한 상승은 필요한 순간에만 하는 구도를 나타내는 그림.필요한 순간에만 의뢰UI 앱(asInvoker)broker(별도 EXE / service)권한 상승이 필요한 처리만 실행broker에 넘기는 입력도 검증한다

그림6: 평소에는 asInvoker로 실행하고, 권한 상승이 필요한 처리만 broker로 모으면 사고 영향 범위가 작아집니다.

3.3. 바이너리와 인스톨러에 서명한다

Windows에서는 배포물의 신뢰성이 결정적입니다. 사용자가 만지는 것은 소스 코드가 아니라 EXE, DLL, MSI, MSIX, 업데이터입니다. 여기가 서명되지 않았으면 운영상 설명도, 변조 탐지도, 배포 시의 안심도 약해집니다.

사용자가 만지는 것은 배포물사용자가 만지는 것은 소스 코드가 아니라 EXE, DLL, 인스톨러, 업데이터이며, 여기가 서명되지 않았으면 변조 탐지나 운영상 설명이 약해진다는 것을 나타내는 그림.사용자가 만지는 것EXE / DLLMSI / MSIX업데이터서명되지 않았으면 변조 탐지도 설명도 약하다

그림7: 공격 표면이 되는 것은 배포물 자체이며, 서명하지 않은 채면 신뢰의 근거를 잃습니다.

최소한 봐 두고 싶은 것은 이 정도입니다.

  • EXE / DLL / MSI / MSIX를 서명한다
  • 인스톨러뿐 아니라 업데이트에 쓰는 보조 바이너리도 서명한다
  • 타임스탬프를 붙인다
  • 인증서 기한과 갱신 절차를 release 절차에 넣는다

특히 타임스탬프를 붙이지 않은 서명은 인증서 기한 만료 후 검증에서 곤란해지기 쉽습니다. 「서명했으니 끝」이 아니라 서명 + 타임스탬프까지를 release 절차에 넣어 두는 편이 안정적입니다.

서명과 타임스탬프타임스탬프를 붙이지 않은 서명은 인증서 기한 만료 후 검증에서 곤란해지기 쉽고, 서명과 타임스탬프까지를 release 절차에 넣으면 안정된다는 것을 나타내는 그림.서명만인증서 기한 만료 후 검증에서 곤란해지기 쉽다서명 + 타임스탬프기한 만료 후 검증에서 곤란해지기 어렵다release 절차에 넣어 둔다

그림8: 서명만으로 끝내지 않고, 타임스탬프까지 포함해 release 절차에 넣어 둡니다.

MSIX를 쓴다면 패키지 서명은 전제입니다. MSI / EXE 배포에서도 적어도 인스톨러 본체와 주요 실행 바이너리는 서명해 두는 편이 좋습니다.

3.4. 업데이트 경로를 고정하고 변조 탐지를 넣는다

요즘 Windows 앱에서는 최초 설치보다 업데이트 경로가 더 오래 쓰입니다. 여기가 느슨하면 본체를 정성껏 만들어도 업데이터가 가장 약한 곳이 됩니다.

업데이트 경로가 가장 오래 쓰인다최초 설치보다 업데이트 경로가 더 오래 쓰이므로, 업데이트 주변이 느슨하면 업데이터가 앱 전체에서 가장 약한 곳이 된다는 것을 나타내는 그림.쓰이는 것은 한 번오래 계속 쓰인다최초 설치앱의 수명업데이트 경로느슨하면 가장 약한 곳이 된다

그림9: 업데이트 경로는 최초 설치보다 오래 쓰이며, 느슨하게 만들면 거기가 최대 약점이 됩니다.

업데이트 주변에서 최소한 생각해 두고 싶은 것은 이 다섯 가지입니다.

  • 업데이트 파일 취득은 HTTPS 전제
  • 다운로드한 업데이트 패키지의 서명이나 해시를 검증한다
  • 업데이트 소스 URL을 코드나 설정으로 무제한 바꿀 수 없게 한다
  • 업데이트 모듈 자체도 서명한다
  • 롤백이나 실패 시 복구 절차를 정한다

MSIX + App Installer를 택할 수 있다면 업데이트 구조를 OS 쪽으로 모으기 쉽습니다. 반면 자체 업데이터를 가진다면 통신의 안전성배포물의 진위 양쪽을 확인해야 합니다. HTTPS만으로는 「통신 경로」는 지켜도 「그 파일이 정말 자신이 발행한 것인지」까지는 보장하지 않습니다.

업데이트에서 확인할 두 가지자체 업데이터에서는 HTTPS에 의한 통신의 안전성과, 다운로드한 업데이트 패키지의 서명이나 해시에 의한 진위 양쪽을 확인해야 한다는 것을 나타내는 그림.업데이트 파일을 HTTPS로 취득서명이나 해시를 검증검증을 통과한 것만 적용HTTPS가 지키는 것은 통신 경로뿐자신의 발행물인지 확인한다

그림10: HTTPS로 받아도 진위는 별개이며, 서명이나 해시 검증을 통과한 뒤에 적용합니다.

3.5. 비밀 정보를 소스 코드나 평문 설정에 두지 않는다

여기는 실무에서 정말 사고로 이어지기 쉬운 곳입니다. 「사내 도구니까」「어차피 exe만 배포하니까」라며 연결 문자열, API 키, 공유 폴더 자격 정보, 고정 토큰을 소스 코드나 설정 파일에 두기 쉽습니다.

최소한 이런 두는 방식은 피하고 싶습니다.

  • 소스 코드에 직접 쓴 API 키
  • appsettings.json이나 app.config의 평문 비밀번호
  • 리포지토리에 들어간 연결 문자열
  • 복호화 키와 암호문을 같은 곳에 두는 설계
  • 사용자별이 아니라 전원 공통의 고정 자격 정보

Windows 앱에서 현실적으로 택할 수 있는 선택은 대략 이 네 가지입니다.

  • Windows의 자격 정보를 저장하고 싶다 packaged desktop app / WinUI 계열이면 Credential Locker를 검토한다
  • 로컬에 비밀을 암호화해 저장하고 싶다 Win32 / .NET이면 DPAPI / ProtectedData를 쓴다
  • 연결 대상이 Windows 인증이나 통합 인증을 쓸 수 있다 가능하면 앱에 비밀번호를 가지게 하지 않는다
  • 클라우드나 서버 쪽에서 비밀을 관리할 수 있다 클라이언트에 장기 비밀을 넣지 않는 설계를 우선한다

C#이라면 적어도 다음과 같이 DPAPI를 쓰기만 해도 평문 저장보다는 꽤 낫습니다. 저장과 읽기의 왕복까지 쓰면 이렇게 됩니다.

// C# / .NET 8. ProtectedData는 Windows 전용이며,
// .NET에서는 NuGet 패키지 System.Security.Cryptography.ProtectedData가 필요합니다.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public static class SecretStore
{
    // 복호화 때에도 같은 값이 필요합니다. null이어도 동작하지만, 붙이는 편이 안전합니다.
    private static readonly byte[] Entropy = [0x4b, 0x53, 0x2d, 0x76, 0x31];

    public static void Save(string path, string secretText)
    {
        byte[] plaintext = Encoding.UTF8.GetBytes(secretText);

        byte[] ciphertext = ProtectedData.Protect(
            plaintext,
            Entropy,
            DataProtectionScope.CurrentUser);

        // 바이트 열 그대로 파일에 두어도 되지만, 설정 파일에 넣을 거면 Base64로 합니다.
        File.WriteAllText(path, Convert.ToBase64String(ciphertext));

        CryptographicOperations.ZeroMemory(plaintext);
    }

    public static string Load(string path)
    {
        byte[] ciphertext = Convert.FromBase64String(File.ReadAllText(path));

        // 저장할 때와 같은 사용자, 같은 entropy가 아니면 CryptographicException이 됩니다.
        byte[] plaintext = ProtectedData.Unprotect(
            ciphertext,
            Entropy,
            DataProtectionScope.CurrentUser);

        try
        {
            return Encoding.UTF8.GetString(plaintext);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(plaintext);
        }
    }
}

호출 쪽은 이렇게 됩니다.

string path = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "SampleApp",
    "token.dat");

Directory.CreateDirectory(Path.GetDirectoryName(path)!);

SecretStore.Save(path, "example-api-key");
string restored = SecretStore.Load(path);

여기서 중요한 것은 「암호화했으니 안전하다」가 아니라 누가 복호화할 수 있는지를 설계로 정하는 것입니다. CurrentUser로 할지 LocalMachine으로 할지에 따라 의미가 꽤 달라집니다. CurrentUser는 저장한 사용자만 복호화할 수 있고, LocalMachine은 같은 머신의 누구든 복호화할 수 있습니다. 서비스로 다른 계정에서 실행하는 경우나 사용자를 바꾸는 운영이 있는 경우에는, 여기를 먼저 정해 두지 않으면 나중에 「읽을 수 없다」「읽어 버린다」 중 한쪽으로 막힙니다.

DPAPI에서 누가 복호화할 수 있는가DPAPI 스코프를 CurrentUser로 하면 저장한 사용자만 복호화할 수 있고, LocalMachine으로 하면 같은 머신의 누구든 복호화할 수 있으므로, 누가 복호화할 수 있는지를 설계에서 먼저 정해야 한다는 것을 나타내는 그림.CurrentUserLocalMachine누가 복호화할 수 있는지를 설계로 정한다저장 사용자만 복호화 가능동일 머신의 누구든 복호화 가능정하지 않으면 나중에 막힌다

그림11: DPAPI는 스코프 선택에 따라 복호화할 수 있는 범위가 달라지므로, 먼저 「누가 복호화할 수 있는지」를 정합니다.

참고로 DPAPI는 사용자 프로필에 키를 두므로, 프로필이 로드되지 않은 상태(impersonation 중 등)에서는 복호화에 실패한다고 문서에 명시되어 있습니다. 서비스에서의 이용을 생각한다면 이 점도 확인해 두십시오.

DPAPI의 키와 사용자 프로필DPAPI는 사용자 프로필에 키를 두므로, impersonation 중처럼 프로필이 로드되지 않은 상태에서는 복호화에 실패한다는 것을 나타내는 그림.프로필 미로드(impersonation 중 등)DPAPI의 키사용자 프로필에 있다복호화에 실패한다서비스에서의 이용에서는 확인 필요

그림12: DPAPI의 키는 사용자 프로필에 있으며, 프로필이 로드되지 않은 상태에서는 복호화할 수 없습니다.

SQL Server 연결이라면 온프레미스 환경에서는 Windows 인증을 첫 후보로 둘 수 있는 경우가 있습니다. 어쩔 수 없이 연결 문자열에 자격 정보를 넣을 거면, 적어도 Persist Security Info=False를 유지하고, 평문 설정 파일에 그대로 두지 않는 편이 안전합니다.

3.6. 통신은 HTTPS 전제, 인증서 검증을 끄지 않는다

개발 중에만 넣었다는 우회가 그대로 프로덕션에 남는다. 통신 주변의 사고는 대개 이 패턴입니다.

특히 출시 빌드에 남기 쉬운 것이 이런 코드나 설정입니다.

  • ServicePointManager.ServerCertificateValidationCallback += ... => true
  • HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
  • 인증서 해지 확인을 무효화한 채로 출시
  • 개발용 자체 서명 인증서 전제의 코드를 프로덕션에 남긴다

최소한의 방침은 단순합니다.

  • 프로덕션 통신은 HTTPS
  • 인증서 검증을 항상 건너뛰지 않는다
  • 예외적으로 검증을 완화해야 한다면 대상 호스트와 인증서를 한정한다
  • 개발용 회피 코드는 빌드 조건이나 설정으로 확실히 제거한다
  • .NET이라면 해지 확인도 의식한다

나쁜 예는 대개 이렇습니다.

ServicePointManager.ServerCertificateValidationCallback +=
    (_, _, _, _) => true;

보기에는 편하지만, 이것은 「이 HTTPS 통신은 누구에게 연결해도 통과시킨다」에 가까운 동작이 됩니다. 인증서 검증을 빼면 HTTPS를 쓰더라도 내용은 꽤 뼈대가 빠집니다.

어느 .NET을 전제로 하는지에 따라 작성 방식이 달라진다

ServicePointManager에 전역 설정을 두는 것은 .NET Framework 시대의 작성 방식입니다. 신규 코드에서는 IHttpClientFactory에서 HttpClient를 받고, TLS 주변 설정이 필요하면 SocketsHttpHandlerHttpClientHandler 쪽에 두는 편이 자연스럽습니다.

다만 「오래된 API이니 이제 적용되지 않을 것」이라고 방치하는 것은 위험합니다. Microsoft 문서에는 ServicePointManager.ServerCertificateValidationCallback.NET 9 이후에는 SocketsHttpHandler.SslOptionsRemoteCertificateValidationCallback에 매핑된다고 적혀 있습니다. 즉, 어딘가 한 줄의 => trueHttpClient 통신까지 통과시켜 버리는 일이 있습니다.

오래된 콜백이 지금의 통신에 적용되는 경로ServicePointManager의 ServerCertificateValidationCallback은 .NET 9 이후 SocketsHttpHandler의 검증 콜백에 매핑되므로, 어딘가 한 줄의 true 반환이 HttpClient 통신까지 통과시켜 버릴 수 있다는 경로를 나타내는 그림..NET 9 이후 매핑된다ServicePointManager의 검증 콜백SocketsHttpHandler 쪽 검증HttpClient 통신에도 적용된다한 줄의 true 반환이 통신 전체의 뼈대를 뺄 수 있다

그림13: 오래된 API에 둔 검증 건너뛰기가 매핑을 통해 지금의 HttpClient 통신까지 통과합니다.

예외적으로 검증을 느슨하게 하고 싶을 때는 프로세스 전체에 적용되는 전역 설정이 아니라 그 핸들러에만 닫힌 형태로 합니다.

// C# / .NET 8. 특정 호스트와 인증서만 예외로 다루는 예입니다.
// 개발용으로 느슨하게 하더라도, 대상을 한정하지 않으면 전역으로 끄는 것과 다르지 않습니다.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

// 대상 인증서의 지문. 설정에서 읽어도 됩니다.
const string ExpectedThumbprint = "2B0C4E6A8D1F3B5D7F9A1C3E5A7C9E1B3D5F7A91";

var handler = new HttpClientHandler
{
    CheckCertificateRevocationList = true,   // 해지를 확인하러 갑니다. 기본값은 false
    ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
    {
        if (errors == SslPolicyErrors.None)
        {
            return true;
        }

        // 인정하는 것은 「이 단말이 사내 CA를 신뢰하지 않는다」는 한 점뿐입니다.
        // 인증서가 오지 않음·호스트 이름이 일치하지 않음은 인정하지 않습니다
        if (errors != SslPolicyErrors.RemoteCertificateChainErrors)
        {
            return false;
        }

        // 상대와 인증서를 고정합니다
        if (request.RequestUri?.Host != "device.internal.example"
            || certificate is null
            || !string.Equals(certificate.Thumbprint, ExpectedThumbprint,
                              StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        // 핀닝한 인증서 하나라도 기한 만료와 해지는 인정하지 않습니다.
        // 인정하는 순간, 「키가 새어 해지한 인증서」를 계속 쓰는 경로가 됩니다
        return chain is not null
            && chain.ChainStatus.All(s =>
                   s.Status is X509ChainStatusFlags.NoError
                            or X509ChainStatusFlags.UntrustedRoot
                            or X509ChainStatusFlags.PartialChain);
    },
};

using var client = new HttpClient(handler);

ExpectedThumbprint는 대상 인증서의 지문을 상수나 설정에서 줍니다.

여기서 중요한 것은 errorschain.ChainStatus를 「못 본 척하지 않는」 것입니다. 지문이 일치했다고 true를 반환해 버리면, 그 인증서가 기한 만료여도, 키가 새어 해지한 뒤여도, 계속 통과합니다. 핀닝은 「이 인증서만 믿는다」는 뜻이지, 「이 인증서면 무슨 일이 있어도 믿는다」가 아닙니다. 위 코드에서 허용하는 것은 UntrustedRootPartialChain(=사내 CA가 이 단말에 들어가 있지 않음)뿐이며, NotTimeValid(기한 만료)나 Revoked(해지)는 그대로 거부가 됩니다.

해지를 보려면 CheckCertificateRevocationList = true가 필요합니다(기본값은 false이며, 해지는 확인되지 않습니다). 반대로, 사내 CA가 CRL도 OCSP도 공개하지 않으면 RevocationStatusUnknown으로 걸러집니다. 그것이 올바른 동작입니다. 해지 확인 수단을 마련할 수 없다면 인증서 유효 기간을 짧게 하거나, 핀닝한 값을 다시 배포할 수 있는 경로를 먼저 만들어 두거나, 둘 중 하나로 메우십시오. 「해지할 수 없는 채로 장기 인증서를 핀닝하는」 것이 가장 위험한 상태입니다.

핀닝 시 판정 흐름오류 없으면 통과하고, 체인 오류 이외는 거부하며, 호스트 이름과 지문을 고정하고, 체인 상태에서도 기한 만료나 해지는 거부한다는 예외 검증의 판정 흐름을 나타내는 그림.None체인 오류 이외체인 오류만불일치일치기한 만료·해지 등사내 CA 미도입 상태만errors를 확인통과거부호스트 이름과 지문을 대조체인 상태를 확인

그림14: 핀닝해도 못 본 척하지 않는 판정 흐름이며, 기한 만료와 해지는 그대로 거부가 됩니다.

3.7. 외부 입력을 전부 「신뢰하지 않는 입력」으로 다룬다

Windows 앱은 Web 앱이 아니라서 입력 validation이 느슨해지기 쉽습니다. 하지만 실제로는 외부 입력의 입구가 생각보다 많습니다.

  • 파일 경로
  • CSV / Excel / JSON / XML
  • 명령줄 인수
  • named pipe / socket / COM / RPC / gRPC
  • DB에 넘기는 문자열
  • 레지스트리 값
  • 클립보드
  • URL / deep link
  • 외부 장치나 SDK에서 돌아오는 데이터

특히 최소한 빠뜨리고 싶지 않은 것은 다음 세 가지입니다.

  1. SQL은 반드시 매개변수화한다 문자열 연결로 SQL을 만들지 않는다.
  2. 파일 경로는 정규화한 뒤에 쓴다 사용자가 지정한 경로를 그대로 삭제, 덮어쓰기, 전개에 쓰지 않는다.
  3. 외부 파일 읽기에는 크기 상한과 형식 검사를 넣는다 「열렸으니 안전하다」가 아니다.

SQL 예로 말하면, 이것은 피하고 싶습니다.

var sql = "SELECT * FROM Users WHERE Name = '" + userName + "'";

최소한도 이렇게 모으고 싶습니다.

using System.Data;
using Microsoft.Data.SqlClient;

using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.Add("@name", SqlDbType.NVarChar, 256).Value = userName;

「사내 도구니까 입력은 신뢰할 수 있다」는 꽤 위험한 전제입니다. 현실에는 깨진 CSV, 예상 밖 파일 이름, 오래된 DB 데이터, 운영자의 수기 입력 실수, 다른 도구가 쓴 어중간한 JSON이 평범하게 들어옵니다.

사내 도구에도 깨진 입력은 온다사내 도구여도 깨진 CSV나 예상 밖 파일 이름, 오래된 DB 데이터, 수기 입력 실수, 어중간한 JSON이 평범하게 들어오므로, 외부 입력은 모두 신뢰하지 않는 입력으로 다룬다는 것을 나타내는 그림.깨진 CSV앱으로의 입력예상 밖 파일 이름수기 입력 실수·오래된 데이터모두 신뢰하지 않는 입력으로 검증

그림15: 사내 도구여도 깨진 입력은 평범하게 오므로, 입구마다 검증한 뒤에 씁니다.

3.8. DLL의 로드 위치를 모호하게 두지 않는다

이것은 Windows 특유의 함정입니다. LoadLibrary("foo.dll")처럼 이름만으로 DLL을 읽히면, 검색 순서에 따라 의도하지 않은 위치의 DLL을 집어올 수 있습니다.

할 일은 정해져 있습니다.

  • 가능하면 DLL의 절대 경로를 지정한다
  • SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)를 이른 단계에서 설정한다
  • AddDllDirectory로 검색 대상을 명시적으로 더한다
  • SearchPath 결과를 그대로 LoadLibrary에 넘기는 설계를 피한다
  • safe DLL search mode에만 기대지 않는다

예를 들어 native code라면 프로세스 초기화의 이른 단계에서 다음을 넣는 설계는 유력합니다.

SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);

그리고 필요한 추가 디렉터리만 AddDllDirectory로 등록합니다.

여기는 「평소에는 동작한다」고 방치되기 쉽지만, 배포처에서 작업 디렉터리가 바뀌거나 다른 제품의 DLL이 PATH에 들어가 있으면 조용히 깨집니다. 보안뿐 아니라 장애 예방으로도 꽤 효과가 있습니다.

DLL 로드 위치의 고정이름만으로 DLL을 읽히면 검색 순서에 따라 의도하지 않은 위치의 DLL을 집어올 수 있으며, 이른 단계에서 SetDefaultDllDirectories를 설정하고 AddDllDirectory로 검색 대상을 명시하며, 가능하면 절대 경로를 지정한다는 대책을 나타내는 그림.이름만으로 로드검색 순서에 따라 의도하지 않은 DLL을 집어온다조기에 SetDefaultDllDirectoriesAddDllDirectory로 명시적으로 추가가능하면 절대 경로 지정로드 위치가 모호해지지 않는다

그림16: 이름에 맡기는 로드를 그만두고, 검색 대상을 명시해 로드 위치를 고정합니다.

3.9. 로그와 예외에 기밀을 내지 않는다

장애 조사를 위해 로그를 늘리는 것은 중요합니다. 다만 로그는 기밀의 무덤이 되기 쉽습니다.

로그 주변에서 최소한 다시 보고 싶은 것은 다음과 같습니다.

  • 비밀번호, Bearer token, API 키를 로그에 내지 않는다
  • 연결 문자열을 통째로 내지 않는다
  • 개인 정보나 업무 데이터 본문은 마스킹한다
  • 예외 상세는 사용자용 화면과 내부 로그로 나눈다
  • debug용 PII 로그를 프로덕션에서 켜지 않는다
  • dump나 trace의 저장 위치 권한을 다시 본다

최근 .NET에서는 redaction을 전제로 한 정리도 하기 쉬워졌습니다. 적어도 「무엇이든 문자열화해 그대로 log」는 그만두고 싶습니다.

흔히 있는 실패를 몇 가지 듭니다.

  • HTTP request / response body를 통째로 저장한다
  • 인증 실패 시 토큰이나 헤더 전체를 출력한다
  • 예외 메시지를 그대로 MessageBox에 낸다
  • 유지보수용 ZIP에 기밀 로그를 전부 동봉한다

오류 표시는 예를 들어 다음과 같이 나눕니다.

  • 사용자용: 「서버 연결에 실패했습니다. 네트워크 설정과 URL을 확인해 주십시오.」
  • 내부 로그: 실패 대상 호스트, TLS 오류 종류, 상관 ID, stack trace, 재시도 횟수

이 분리만으로도 정보 유출과 조사성의 균형이 꽤 좋아집니다.

오류 표시의 구분오류가 났을 때 사용자에게는 간결한 안내만 내고, 내부 로그에는 실패 대상 호스트나 오류 종류, 상관 ID, stack trace 등 조사 정보를 남긴다는 구분을 나타내는 그림.오류 발생사용자용: 간결한 안내만내부 로그: 호스트·종류·상관 ID 등유출을 막으면서 조사성을 유지한다

그림17: 사용자용 표시와 내부 로그를 나누는 것만으로, 유출과 조사성의 균형이 좋아집니다.

3.10. 의존 라이브러리와 개발 도구를 방치하지 않는다

마지막은 수수하지만 효과가 큰 항목입니다. 앱 본체를 정성껏 만들어도, 오래된 런타임이나 알려진 취약성이 있는 의존 라이브러리를 그대로 쌓아 두면 기반이 무너집니다.

볼 항목 자체는 많지 않습니다.

  • .NET SDK / runtime을 지원 범위 안의 판으로 유지한다
  • NuGet / OSS 의존의 업데이트를 정기적으로 확인한다
  • C++라면 런타임 재배포물이나 외부 DLL의 판 관리를 한다
  • 취약성 정보 확인을 release 전 체크에 넣는다
  • 의존 업데이트로 깨지지 않도록 smoke test를 준비한다

여기는 「나중에 한꺼번에 한다」가 가장 위험합니다. 반년, 1년 방치하면 업데이트 차이가 너무 커져, 보안 대응 자체가 무거운 작업이 됩니다.

의존 업데이트를 방치한 경우의존 관계의 업데이트를 반년에서 1년 방치하면 업데이트 차이가 너무 커져, 보안 대응 자체가 무거운 작업이 된다는 흐름을 나타내는 그림.이것을 피할 수 있다나중에 한꺼번에 한다반년~1년의 방치업데이트 차이가 너무 커진다대응 자체가 무거운 작업이 된다정기적으로 확인한다

그림18: 의존 업데이트는 방치할수록 차이가 커지고, 대응 자체가 무거운 작업이 되어 갑니다.

3.11. 각 항목을 어떻게 확인하는가

체크리스트는 확인 방법과 세트로 있어야 비로소 기능합니다. 3.2부터 3.10의 항목에 대해, 릴리스 전에 실제로 돌려 볼 수 있는 것을 나열해 둡니다.

확인하고 싶은 것 확인 방법
권한 상승을 요구하지 않는가 앱 매니페스트에서 requestedExecutionLevel 값을 본다. 소스라면 app.manifest, 배포물이라면 Sysinternals의 Sigcheck나 리소스 에디터로 확인한다
서명과 타임스탬프 PowerShell에서 Get-AuthenticodeSignature .\app.exe를 실행하고, StatusValid인지, TimeStamperCertificate가 들어 있는지를 본다. 동봉 DLL과 updater도 하나씩 본다
인증서 검증을 끄지 않았는가 소스 전체를 ServerCertificateValidationCallback, DangerousAcceptAnyServerCertificateValidator, ServerCertificateCustomValidationCallback, CheckCertificateRevocationList로 검색한다
비밀 정보의 직접 기입 Password=, ApiKey, Secret, Token, ConnectionString으로 검색한다. 현재 소스뿐 아니라 리포지토리 이력도 대상으로 한다
SQL의 조립 "SELECT, "INSERT, + 를 포함하는 문자열 연결을 검색하고, Parameters.Add를 거쳤는지 확인한다
DLL의 로드 위치 Process Monitor에서 대상 프로세스로 좁히고, Path ends with .dllResult is NAME NOT FOUND 필터를 건다. 어디를 어떤 순서로 찾으러 갔는지가 보이므로, 의도하지 않은 폴더를 보지 않았는지 확인한다
의존 관계의 알려진 취약성 dotnet list package --vulnerable --include-transitive를 실행한다
로그에 기밀이 나가지 않았는가 한 번 실행한 뒤, 출력된 로그를 Bearer , Password, Authorization으로 검색한다

코드 검색은 rg(ripgrep)여도 Visual Studio 검색이어도 상관없습니다. 한꺼번에 돌린다면 이 형태입니다.

Get-ChildItem -Recurse -Include *.cs,*.vb,*.cpp,*.h,*.config,*.json |
    Select-String -Pattern 'ServerCertificateValidationCallback|DangerousAcceptAnyServerCertificateValidator|Password=|ApiKey' |
    Select-Object Path, LineNumber, Line

중요한 것은 확인한 것 자체를 기록에 남기는 것입니다. 「검색했다」「0건이었다」까지 남겨 두면, 다음 릴리스에서는 차이만 보면 됩니다.

확인을 기록에 남기는 효과릴리스 전 확인은 결과가 0건이어도 기록에 남겨 두면, 다음 릴리스에서는 차이만 보면 되게 된다는 흐름을 나타내는 그림.릴리스 전에 확인을 실행검색했다·0건이었다까지 기록다음 릴리스는 차이만 보면 된다

그림19: 「확인했다」는 사실까지 기록해 두면, 다음부터는 차이 확인만으로 끝납니다.

4. 릴리스 전 체크리스트

리뷰나 출시 판정의 템플릿으로 그대로 쓸 수 있는 형태로 합니다. 표로 확인하기 쉽도록, 릴리스 전에 최소한 보고 싶은 항목을 카테고리별로 나열합니다.

4.1. 권한, 실행 방식

체크 항목 확인 메모
일반 실행은 asInvoker로 동작한다  
관리자 권한이 필요한 처리는 별도 EXE / service 등으로 분리했다  
service를 쓰는 경우, 필요 이상으로 강한 실행 계정으로 두지 않았다  
%ProgramFiles% 아래와 사용자 데이터 아래의 책임을 나눴다  

4.2. 배포, 서명

체크 항목 확인 메모
EXE / DLL / MSI / MSIX / updater에 서명했다  
서명에 타임스탬프를 붙였다  
인증서 기한과 갱신 절차를 release 흐름에 넣었다  
배포물의 해시 확인이나 변조 탐지 방법이 정해져 있다  

4.3. 업데이트

체크 항목 확인 메모
업데이트 취득은 HTTPS로 한다  
다운로드 후 서명 또는 해시를 검증한다  
업데이트 소스 URL을 마음대로 바꾸기 어려운 설계다  
업데이트 실패 시 롤백 또는 재시도 방침이 있다  

4.4. 비밀 정보

체크 항목 확인 메모
비밀번호, API 키, 연결 문자열을 소스 코드에 직접 쓰지 않았다  
평문 설정 파일에 비밀을 두지 않았다  
로컬 저장이 필요한 비밀은 DPAPI / Credential Locker 등으로 보호한다  
가능한 곳은 Windows 인증이나 사용자 자격 정보로 모았다  

4.5. 통신

체크 항목 확인 메모
프로덕션 통신은 HTTPS를 쓴다  
DangerousAcceptAnyServerCertificateValidator=> true를 출시 빌드에 남기지 않았다  
해지 확인이나 호스트 이름 검증을 의식한다  
개발용 인증서 전제의 코드나 설정이 프로덕션에 섞이지 않았다  

4.6. 입력, 데이터 액세스

체크 항목 확인 메모
SQL은 매개변수화했다  
명령줄, 파일, IPC, URI 등의 입력에 상한과 형식 검사가 있다  
경로 조작은 정규화해 루트 이탈을 막는다  
예외 메시지를 그대로 화면에 내지 않았다  

4.7. DLL과 실행 환경

체크 항목 확인 메모
DLL의 로드 위치를 명시했다  
SetDefaultDllDirectories / AddDllDirectory 등으로 검색 순서를 제어한다  
현재 디렉터리나 PATH에 맡기는 DLL 로드를 하지 않았다  
배포처에서 동적 로드에 필요한 파일들을 파악하고 있다  

4.8. 로그, 운영

체크 항목 확인 메모
토큰, 비밀번호, PII를 로그에 내지 않았다  
내부 로그와 사용자용 메시지를 나눴다  
dump / trace / log의 저장 위치 권한을 다시 봤다  
SDK와 의존 라이브러리의 업데이트 상황을 확인했다  

5. 흔히 있는 NG

실무에서 자주 보는 것은 대개 이런 착각입니다.

5.1. 「사내 도구니까 괜찮다」

사내 도구여도 깨진 파일, 오조작, 반입 단말, 공유 폴더, 오래된 DLL, 느슨한 권한 설정은 평범하게 있습니다. 인터넷에 공개하지 않아도 공격 표면은 사라지지 않습니다.

5.2. 「HTTPS니까 안전하다」

HTTPS는 중요하지만, 인증서 검증을 무효화하면 의미가 꽤 옅어집니다. 또한 업데이트 배포에서는 HTTPS뿐 아니라 배포물의 진위 확인도 필요합니다.

5.3. 「암호화했으니 안전하다」

복호화 키의 위치, 복호화 권한, 사용자 경계, 머신 경계가 정리되어 있지 않으면 암호화만으로는 부족합니다. 특히 LocalMachine으로 보호한 값을 「사용자별 비밀」이라고 생각하고 쓰면 나중에 혼란스럽습니다.

5.4. 「로그를 늘리면 조사할 수 있다」

로그만 많고 토큰이나 개인 정보가 흘러나오면, 그것 자체가 인시던트가 됩니다. 조사성이 필요하다면 무엇을 남기고 무엇을 가릴지를 정하는 편이 먼저입니다.

5.5. 「관리자로 돌리면 해결」

처음에는 편하지만, 나중에 UAC, 배포, 지원, 권한 경계, DLL 로드, 파일 저장 위치에서 대개 힘들어집니다. 최소 권한이 장기적으로는 안정적입니다.

「관리자로 돌리면 해결」의 결말관리자 권한으로의 상시 사용은 처음에는 편해도, 나중에 UAC나 배포, 지원, 권한 경계, DLL 로드, 파일 저장 위치에서 힘들어지며, 최소 권한이 장기적으로는 안정된다는 것을 나타내는 그림.장기적으로는 안정관리자로 돌리면 해결처음에는 편하다UAC·배포·지원에서 힘들어진다권한 경계·저장 위치에서 힘들어진다최소 권한으로 실행힘듦을 피할 수 있다

그림20: 관리자 상시 사용의 편함은 처음뿐이며, 장기적으로는 최소 권한이 안정적입니다.

6. 대략적인 우선순위

전부를 한 번에 하기가 무겁다면, 우선순위는 대개 이렇게 됩니다.

나열하는 기준은 사고 났을 때의 피해 크기고치는 비용의 낮음의 곱입니다. 3장은 「권한 → 배포 → 구현 → 운영」이라는 설계의 흐름으로 나열했지만, 이쪽은 「위험한 것부터」의 순서이므로 장의 순서와는 일치하지 않습니다. 대응하는 절을 붙여 둡니다.

  1. 관리자 권한의 재검토(3.2) 먼저 requireAdministrator의 상시 사용을 그만둔다. 피해 범위가 한 단계 바뀌는 데 비해, 설계 변경으로는 작게 끝나는 경우가 많다.
  2. 서명과 타임스탬프(3.3) 배포물의 신뢰성을 갖춘다. 절차에 넣는 것만으로 끝나고, 나중에 넣으면 재배포가 필요하다.
  3. 비밀 정보 빼내기(3.5) 소스 코드, 평문 설정에서 비밀을 뺀다. 새었을 때의 피해가 크고, 샌 뒤에는 되돌릴 수 없다.
  4. HTTPS + 인증서 검증 바로잡기(3.6) => true 계열을 출시 빌드에서 지운다. 삭제하는 것만으로 고쳐지는 경우가 많고, 방치하면 통신을 통째로 믿을 수 없다.
  5. SQL / 파일 / IPC 입력의 재검토(3.7) 문자열 연결이나 무검증 입력을 줄인다. 건수는 많지만, 한 곳씩 고칠 수 있다.
  6. DLL 로드의 고정(3.8) 이름만 로드, PATH에 맡기기를 그만둔다. 기동 처리의 일부를 고치는 작업이며, 장애 예방에도 된다.
  7. 로그의 마스킹(3.9) 사고 시에 로그가 2차 재해가 되지 않게 한다. 출력 곳이 많은 만큼 시간은 걸린다.
  8. 의존 업데이트의 정례화(3.10) 릴리스마다 확인하는 흐름으로 만든다. 한 번에 끝나지 않으므로, 구조로 만들기까지가 일이다.

업데이트 경로(3.4)가 이 나열에 들어 있지 않은 것은, 자동 업데이트가 없는 앱도 있기 때문입니다. 있다면 2번의 서명과 같은 우선순위로 보십시오. 업데이트 모듈은 본체보다 약한 데 본체를 덮어쓸 수 있는 입장에 있습니다.

우선순위를 매기는 방법사고 났을 때의 피해 크기와 고치는 비용의 낮음의 곱으로 위험한 것부터 나열하고, 자동 업데이트가 있는 앱에서는 업데이트 경로를 서명과 같은 우선순위로 본다는 생각을 나타내는 그림.피해의 크기곱으로 순위를 정한다고치는 비용의 낮음위험한 것부터 막는다자동 업데이트가 있으면 업데이트 경로는 서명과 같은 우선순위

그림21: 우선순위는 피해 크기와 고치는 비용의 곱으로 정하고, 위험한 구멍부터 막습니다.

이 순서라면 「우선 명백히 위험한 구멍을 막는다」는 의미에서 진행하기 쉽습니다.

7. 정리

Windows 앱 개발의 보안은, 특별한 제품이나 거대한 구조를 넣기 전에 권한, 서명, 비밀 정보, 통신, 입력, DLL, 로그 일곱 가지를 갖추는 것만으로도 꽤 달라집니다.

최소한의 기준을 한 마디씩으로 하면 이렇게 됩니다.

  • 앱 전체를 관리자 권한으로 실행하지 않는다
  • 배포물과 업데이트 패키지에 서명하고, 타임스탬프를 붙인다
  • 비밀 정보를 소스 코드나 평문 설정에 두지 않는다
  • HTTPS를 쓰더라도 인증서 검증을 끄지 않는다
  • SQL, 파일, IPC 등의 외부 입력을 신뢰하지 않는다
  • DLL의 로드 위치를 모호하게 두지 않는다
  • 로그에 기밀을 내지 않는다
  • 의존 라이브러리를 방치하지 않는다

보안 이야기는 넓지만, 처음부터 전부를 할 필요는 없습니다. 다만 위험한 기본 동작을 그대로 출시하지 않는다는 최소한만은, 꽤 이른 단계에서 갖출 가치가 있습니다.

8. 참고 자료

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

Windows 앱 개발

권한 설계, 배포 방식, 업데이트 방식, 로그 설계까지 포함해 Windows 앱 전체를 다시 보는 이야기이므로, Windows 앱 개발과 잘 맞는 주제입니다.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

Windows 앱 보안에서 가장 먼저 확인해야 할 것은 무엇인가요?
불필요한 관리자 권한을 요구하지 않는 것, 코드 서명하는 것, 비밀 정보를 평문으로 두지 않는 것, 인증서 검증을 무효화하지 않는 것, 이 네 가지입니다. 최소한의 보안은 특수한 기능을 더하는 것보다 위험한 기본 동작이나 허술한 구현을 남기지 않는 것입니다. 인증서 검증을 항상 건너뛰기, 평문 연결 문자열, 현재 디렉터리에 맡기는 DLL 로드, 문자열 연결로 SQL을 실행하는 것은 최소한의 기준에서도 피해야 할 항목입니다.
앱 전체를 관리자 권한으로 실행하면 안 되나요?
피해야 합니다. 앱 전체를 관리자 권한으로 실행하면 버그, DLL 바꿔 치기, 설정 파일 오독, 외부 입력의 허점이 그대로 강한 권한으로 실행됩니다. 일반 UI 앱은 asInvoker를 기본으로 하고, 관리자 권한이 필요한 처리만 별도 프로세스나 service로 분리해, 권한 상승은 필요한 순간에만 하는 것이 기본 방침입니다.
API 키나 연결 문자열은 어디에 저장해야 하나요?
소스 코드나 appsettings.json 같은 설정 파일에 평문으로 두는 상태는 우선 벗어나야 합니다. 저장 시 기밀 정보는 용도에 따라 DPAPI / ProtectedData나 Credential Locker를 구분해 사용합니다. 또한 로그에 토큰, 비밀번호, 연결 문자열, 개인 정보를 그대로 남기면 로그 자체가 사고의 주역이 되므로 마스킹이 필요합니다.
사내 배포 앱에도 코드 서명이 필요한가요?
전제로 생각하는 편이 좋습니다. Windows 앱에서는 배포물 자체(EXE / DLL / MSI / MSIX / 자동 업데이트 모듈)가 공격 표면이 됩니다. 코드 서명+타임스탬프가 있으면 변조 탐지, 사용자에 대한 신뢰, 운영상 설명의 용이함을 얻을 수 있습니다. 업데이트 메커니즘도 업데이트 소스를 고정하고, HTTPS와 서명 확인으로 변조를 탐지할 수 있는 형태여야 합니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기