[보안/SER-004] 웹취약점 점검 조치 5건 — 휴면해제·CSRF·난수·하드코딩·보안헤더 #1

Open
somang4819 wants to merge 0 commits from feature/privacy-access-control-policy into master
Owner

개요

2026 문화품앗이 개인정보보호 기능개선 사업 SER-004(시큐어코딩·웹취약점 점검) 조치. 신규·변경 기능 대상 웹취약점 점검(OWASP ZAP + 수동)에서 확인된 5건을 조치하고 로컬에서 실증 검증했다.

조치 내역

취약점 등급 커밋
인증 없는 휴면계정 해제 (CWE-306) 긴급 a085956
CSRF 토큰 부재 (CWE-352) 중 d75c0da
예측 가능한 난수 (CWE-330) 중 a085956, e82e061
계정정보 하드코딩 (CWE-798) 중 6b57520
보안 헤더 누락 (CWE-693) 하 6b57520

검증

  • 휴면해제: 무인증 공격 PoC 재현 → 조치 후 403/거부 확인
  • CSRF: cross-origin 위조 → 403 차단, same-origin 정상 통과(회귀 없음)
  • 보안헤더: X-Frame-Options SAMEORIGIN, X-Content-Type-Options nosniff 응답 확인

배포 주의

  • egov-com-servlet.xml, web.xml 변경 → 전체 재기동 필요
  • globals-.properties(gitignore)에 Globals.sms.jdbc. 키 환경별 별도 설정 필요

🤖 Generated with Claude Code

## 개요 2026 문화품앗이 개인정보보호 기능개선 사업 SER-004(시큐어코딩·웹취약점 점검) 조치. 신규·변경 기능 대상 웹취약점 점검(OWASP ZAP + 수동)에서 확인된 5건을 조치하고 로컬에서 실증 검증했다. ## 조치 내역 | 취약점 | 등급 | 커밋 | |---|---|---| | 인증 없는 휴면계정 해제 (CWE-306) | 긴급 | a085956 | | CSRF 토큰 부재 (CWE-352) | 중 | d75c0da | | 예측 가능한 난수 (CWE-330) | 중 | a085956, e82e061 | | 계정정보 하드코딩 (CWE-798) | 중 | 6b57520 | | 보안 헤더 누락 (CWE-693) | 하 | 6b57520 | ## 검증 - 휴면해제: 무인증 공격 PoC 재현 → 조치 후 403/거부 확인 - CSRF: cross-origin 위조 → 403 차단, same-origin 정상 통과(회귀 없음) - 보안헤더: X-Frame-Options SAMEORIGIN, X-Content-Type-Options nosniff 응답 확인 ## 배포 주의 - egov-com-servlet.xml, web.xml 변경 → 전체 재기동 필요 - globals-*.properties(gitignore)에 Globals.sms.jdbc.* 키 환경별 별도 설정 필요 🤖 Generated with [Claude Code](https://claude.com/claude-code)
somang4819 added 25 commits 2026-08-31 02:28:57 +00:00
접속기록(SFR-004/#33):
- AccessLogQueue 포화 방어. 종전 offer(timeout) 무한 반복은 컨슈머 정지 시
  요청 스레드가 영구 대기해 WAS 스레드 풀 고갈 → 사이트 전면 정지로 이어졌다.
  대기를 3회로 제한하고 포화 차단기(saturated)를 도입해 지속 포화 시 즉시 판정한다.
- 유실은 조용히 넘기지 않는다. recordDrop 이 누적 집계 + 초기 20건/이후 100건 표본
  ERROR 로그로 남긴다(법정 보관 대상이므로 사후 추적 가능해야 함).
- 컨슈머 루프가 Exception 이 아닌 Throwable 까지 흡수한다. Error 하나로 컨슈머가
  죽으면 running=true 인 채 이후 전 요청이 포화 대기에 빠졌다.
- 자체 데몬 워치독(30초) 추가 — 컨슈머 사망 시 재기동, 적체 경고, 차단기 자동 해제.
  task:annotation-driven 활성화 여부와 무관해야 하므로 @Scheduled 를 쓰지 않는다.
- 해시체인 순번 보장을 위한 단일 컨슈머 전제는 유지(병렬화 금지).

인증정보 보호(SER-001/SER-003/#27):
- EgovFileScrty 사용자별 랜덤 솔트(S1: 형식) 도입, matches() 로 검증 일원화 —
  기존 무솔트 계정과 공존한다.
- 비밀번호 변경 시 현재 비밀번호 검증 복구. 종전에는 DB 값을 자기 자신과 비교해
  항상 true 였고, 입력한 oldPassword 가 어디에도 쓰이지 않았다.
- 평문 비밀번호 println 제거.

관리자 화면: 접속기록 조회, 업무구분 매핑 관리, 권한변경 이력, 미사용계정 현황.
계정 파기(AccountDelete) 서비스 및 DDL/DML 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5개 세션에 걸쳐 쌓인 미커밋 변경분을 한 번에 정리한다. 주 산출물은 이번 세션의
접근통제 정책 적용(SER-001 요건 4·5)이고, 나머지는 앞선 세션들의 백로그다.

■ 접근통제 정책 적용 (SER-001 요건 4·5, 이번 세션)
「개인정보보호강화 파일럿 개발항목」 접근통제 정책 적용(Action2) 시안 반영.

- ACCESS_CONTROL_POLICY(단일행) + 관리자 화면 신규 (메뉴 9100047)
  · 계정 잠금: 인증 실패 임계값(10 -> 5), 잠금 해제 방식(관리자화면+이력 / +사유필수)
  · 자동 로그아웃: 미사용 시간(60 -> 30분), 만료 임박 안내 사용여부·시점
  · 세션 재발급: 2차 인증 성공 시 세션 고정(Session Fixation) 차단 여부
- SessionPolicyInterceptor 신규 — /mnt/ 요청마다 정책값으로 세션 타임아웃 적용.
  web.xml 재배포 없이 값이 바뀐다(2025.9 자료요청 3번 "시간 설정 기능 없음" 해소).
  web.xml <session-timeout>30 은 이제 폴백값이다.
- 잠금 임계값 저장 시 인증 경로가 읽는 LOGIN_LOCK_COUNT 를 함께 동기화.
  정책 테이블만 고치면 화면 숫자만 바뀌고 실제 인증은 옛 값을 계속 쓰기 때문이다.
- 관리자 상단(top.jsp): 로그인 후 경과시간 + 자동 로그아웃 잔여 카운트다운,
  만료 임박 안내와 연장(/mnt/session/extend.do).
  세션 상태를 주기 폴링하지 않는다 — 그 요청 자체가 세션을 갱신해
  '미사용 자동 로그아웃'이 영원히 오지 않기 때문이다.
- 자동 로그아웃 시 로그인 화면 이동: actionLogout.do?expired=Y 로 세션 invalidate 후
  로그인 화면 + 사유 안내. 수동 로그아웃(프론트 메인 이동)은 기존 동작 유지.
- 계정잠금관리: 정책이 '사유 필수'면 해제 사유 강제, ACCOUNT_UNLOCK_HISTORY 적재.

■ EgovLoginLogAspect fail-safe (회귀 수정)
접속기록 적재 예외가 @Before 어드바이스 밖으로 나가 로그인·로그아웃이 통째로 500 이
되고 있었다(로컬: MySQL 8 + mysql-connector 5.1.17 의 tx_isolation 조회 실패).
클래스 Javadoc 이 이미 fail-safe 를 명세하고 있었으나 구현이 따르지 않았다.
try/catch 로 인증 흐름을 살리되, 접속기록 유실은 SFR-004 직결이라 ERROR 로 남긴다.

■ 앞선 세션 백로그
- 접속기록(SFR-004): 조회·CSV 비동기 다운로드(PDAL_DOWNLOAD_JOB), 업무유형 매핑
- 로그인 접속기록(#62): COMTNLOGINLOG 접속IP·접속ID 유실 복구 + 접속위치 스냅샷
- 권한관리(SFR-001): 권한변경 이력 CSV 비동기 다운로드, 신청·승인 워크플로 화면 정리
- 미사용계정(SFR-002): 현황 화면 시안 반영(요약·역할·잠금/말소·조치 이력)
- 회원정보 암호화(추가제안 Action4): AES 암복호화·검색용 해시, 이관 배치와 사전 백업

■ 커밋 제외
font/(글꼴 설치본 22M), 제안서 문서(pptx/hwp), .claude/(하네스) 는 소스가 아니라 제외.
.claude/ 버전관리 여부는 별도 판단이 필요하다.

배포 산출물: _workspace/deploy/20260810_1300/ (DELTA.md, sql/, APPLY_LOG.md)
브라우저 검증(2단계 인증 필요)은 APPLY_LOG.md B1~B7 로 남겨둔다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
일반회원(GNR)은 탈퇴 즉시 하드 DELETE되어 휴면 전환 기반 지연 파기 대상이
아니었으므로 배치에서 제외. 관리자(개인정보취급자) 계정은 SFR-002 핵심요건
(개인정보보호위 점검 지적사항 — 휴면 대상이 일반회원에 한정되고 개인정보
취급자가 미포함)이라 로직은 그대로 유지하고 대상만 EMP로 좁혔다.
정책 화면(계정 관리 정책) 안내 문구도 관리자 계정 대상임을 명시.

스키마 변경 없음(ACCOUNT_MNG_POLICY.DELETE_PRESERVE_DAY/PURGE_USE_YN,
ACCOUNT_DELETE_HISTORY 재사용).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
미사용계정 현황(#11, 메뉴 9100037)이 대상이 개인정보취급자(EMP) 계정임을
화면·메뉴명에서 알 수 없어 오해 소지가 있었다. "장기 미사용 계정" →
"장기 미사용 관리자 계정"으로 명시(요약표/목록 캡션, 메뉴명, 프로그램 설명).
이미 배포된 환경을 위해 재실행 안전한 UPDATE 문을 DML에 추가.

목록 행별 "말소"/"잠금" 버튼은 시각적으로 과도하게 커서 축소(.btn3.mini).
상단 "선택 말소" 일괄 버튼은 그대로 둠.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
관리자 계정이 5회 실패로 잠기면 해제 화면도 로그인이 있어야 들어갈 수 있어
아무도 풀 수 없는 상태가 될 수 있었다. 지정 계정을 LOGIN_LOCK_EXEMPT
화이트리스트로 등록하면 실패 잠금 판정만 면제되고, LOGIN_HISTORY 감사이력과
2단계 인증(OTP)은 예외 없이 그대로 적용된다. 캐시는 fail-closed로 조회
실패 시 전 계정 정상 잠금 판정을 유지한다.

접근통제 정책 화면에 '잠금 예외 계정' 섹션을 추가해 등록/해제하며,
같은 화면의 '잠금 해제 방식'은 실사용 중인 단일 옵션만 남기도록 단순화했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
일반회원 이름(COMTNGNRLMBER.MBER_NM) 암호화 저장(추가제안 Action4)의 마지막 단계로,
comvnusermaster/comvnuserinfo 뷰를 거쳐 이름을 표시·검색하는 전체 소비자(66개 표시
구문 + 신규 발견된 검색(LIKE/exact-match) 구문)를 뷰 재정의 이전에 전수 정비했다.

- 표시: 판별 컬럼(USER_SE 등) 추가 후 GNR일 때만 조건부 복호화
- 검색: LIKE/exact-match를 (HASH = #hash# OR 원본 조건) 패턴으로 전환
- 과정에서 발견한 기존 버그(EgovBBSLoneMaster/EgovBoard의 columnIndex 위치
  불일치, BizPrvol/EgovLoginUsr/BizPrapp의 누락된 검색 전환 지점)를 최소 범위로
  수정하거나(내 필드에 한해) 영향받지 않도록 우회 처리

뷰 자체(ALTER VIEW)는 아직 미실행 — 모든 소비자 정비가 끝나야 실행 가능한
all-or-nothing 게이트가 있어 별도로 진행한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
회원정보 암호화(Action4)의 마지막 단계. 545b49d로 반영한 소비자 정비(66개 표시 +
전체 검색전환)를 전제로, 두 공유 뷰의 GNR 분기를 MBER_NM(평문)에서 MBER_NM_ENC(암호문)로
전환하고 검색용 USER_NM_HASH 컬럼을 추가하는 DDL을 127.0.0.1:3306 dev DB에 실제 실행했다.

- information_schema.views.VIEW_DEFINITION을 서버사이드 REPLACE()로 최소 치환(PREPARE/
  EXECUTE) — comvnuserinfo의 한글 리터럴(USER_GUBUN_NM)을 클라이언트에서 재입력하지
  않아 터미널 인코딩 문제를 원천 회피
- 실행 전 원본 정의를 privacy_view_def_backup 테이블에 백업, 치환 건수/컬럼 존재
  여부를 assertion으로 강제해 잘못된 정의 반영을 차단
- 행 수(USER_SE/USER_GUBUN별 COUNT) 실행 전후 동일 확인, GNR 표본 암호문+해시 노출
  확인, USR/ENT 표본 평문 그대로 유지 확인 — 상세 로그는 _workspace/deploy/20260811_0850/
  DB_APPLY_LOG.md(gitignored, 로컬 전용)

주의: 이번엔 코드(545b49d) 커밋 후 로컬 WAS 재기동 전에 DB DDL을 먼저 실행했다 —
다음 반영 시 코드부터 배포하고 재기동할 것(LAST_DEPLOYED.md 참조).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
545b49d(4차 델타)에서 EgovDtaUseStats_SQL_Mysql.xml resultMap에
userNmSe 컬럼 매핑만 추가하고 대응 Java 필드/DAO 조건부복호화 배선을
누락했다. iBatis2는 이 프로퍼티 부재를 컴파일타임이 아닌 Spring
컨텍스트 기동시 sql-map-config 전체 파싱 단계에서 검증하므로,
컴파일은 성공했지만 실제 WAS 재기동 시 ProbeException으로 컨텍스트
전체가 기동 실패해 로컬 ROOT가 다운되는 사고로 이어졌다.

- DtaUseStats.java: userNmSe 필드 + getter/setter 추가
- DtaUseStatsDAO.selectDtaUseStats(): GNR일 때만 조건부 복호화 배선

재기동 후 컨텍스트 정상 기동 확인(오류 0건), /frt/main.do 200,
/mnt/sec/accountPolicy/policyView.do 302로 검증.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
csv.wise.ai.kr 배포 파이프라인 신설 (PC → git → web → was)
Some checks failed
deploy-wise / deploy (push) Failing after 11s
ddacd1b7f5
웹서버(175.118.124.176)의 Gitea 러너가 JDK8 로 WAR 을 빌드해 WAS(27.255.76.2)의
Tomcat·MariaDB 컨테이너로 배포한다. WAS 는 웹서버를 통해서만 닿고, Tomcat 은
127.0.0.1 바인딩이라 웹서버가 SSH 터널로 잇는다.

- WAS 는 가용 메모리가 3G 안팎(llama-server 200G+ 점유)이라 빌드를 웹서버에서 한다
- claudeops 가 sudo 를 쓸 수 없어 Tomcat·MariaDB 를 컨테이너로 올렸다
- 배포 스크립트를 매 실행마다 저장소본으로 덮어써 서버 사본 드리프트를 막는다

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
메인 화면 하나가 woff 폰트만 3MB 넘게 끌어오는데 압축도 캐시 헤더도 없었다.
유선망에서는 티가 안 나지만(같은 폰트 0.15초) 테더링에서는 25초가 걸려
브라우저가 로딩을 끝내지 못하고 "접속 안 됨"처럼 보인다.

- mod_deflate: HTML 52,665B → 9,516B (82% 감소)
- 폰트 1년 immutable, 이미지 7일, css/js 1시간 캐시 — 재방문 시 3MB 재다운로드 제거

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
원본 MySQL 8과 동작을 맞추려고 이 모드를 켰던 것이 역효과였다. MySQL 8은
GROUP BY 컬럼에 대한 함수 종속성을 판정해 집계에 없는 컬럼도 통과시키지만
MariaDB는 그 판정을 하지 않는다. 같은 sql_mode를 그대로 옮기면 로컬에서
잘 돌던 쿼리가 서버에서만 터진다.

증상: 관리자 2차인증 로그인 직후 /mnt/main.do 가 404. 실제로는 대시보드
쿼리가 'Z.STATUS1' isn't in GROUP BY 로 실패하고 예외 처리가 404로 이어진 것.

ONLY_FULL_GROUP_BY 제외는 MariaDB의 기본값이기도 하다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
관리자 메뉴 9100045(업무유형 매핑 관리)·9100046(로그인 접속기록) 두 화면을
폐기하고 관련 5계층·sqlMap·JSP를 제거했다. 접속기록은 PERSONAL_DATA_ACCESS_LOG
경로로 일원화한다.

- JobMapping 5계층(Controller/DAO/Service/Impl/Resolver/SearchVO)과
  ConectLog 5계층, PersonalDataLogAspect 제거
- PrivacyJobMapping_SQL_{Mysql,Oracle}.xml 제거 + sql-map-config 등록 라인 삭제
- left.jsp의 9100045/9100046 분기와 상위 하이라이트 조건 제거
- 메뉴 폐기 DML 신설(권한매핑→메뉴→프로그램 순, 멱등) — 종전 등록 DML 2건 대체.
  JSP와 반드시 동반 배포해야 URL 직접 접근 경로가 남지 않는다
- privacy.install_mysql.sql에 PART 4(뷰 GNR 컷오버) 반영, 메뉴 번호 갱신
- MemberNmCryptoController 신설 — 회원 이름 암호화 백필 트리거
- SessionPolicyInterceptor에 stampLogin/clearLoginStamp 추가
  (EgovLoginController가 참조 — 이게 없으면 컴파일되지 않는다)
- pom: mysql-connector를 5.1.17로 고정(8.0.20 비활성화). 배포 환경의
  WEB-INF/lib 실측값과 일치시켜 WAR 배포 시 드라이버가 바뀌는 지뢰를 제거

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
파이프라인: find | head -1 의 SIGPIPE 로 배포가 멈추던 것 수정
All checks were successful
deploy-wise / deploy (push) Successful in 1m5s
7f3c86c588
act 는 run: 스텝을 bash -e -o pipefail 로 실행한다. `find ... | head -1` 은
head 가 첫 줄을 받고 파이프를 닫는 순간 find 가 SIGPIPE(141)로 죽어 파이프라인
전체가 실패로 판정된다. 빌드가 BUILD SUCCESS 로 끝나고 192MB WAR 까지 만든 뒤
"산출물 검증" 스텝이 출력 한 줄 없이 죽어 배포가 멈췄다.

실측: bash -e -o pipefail 에서 `find | head -1` → 141, `-print -quit` → 0.

- 산출물 검증: find 를 -print -quit 로 바꿔 파이프 자체를 없앤다
- 사전 점검: `free -h | head -2` 도 같은 경합을 안고 있어 파이프를 제거

리포 주석이 경고하던 바로 그 함정이다(deploy/dev 배포 스크립트의
unzip -l | grep -q 사고와 동일 원인).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DB 커넥션 풀에 유효성 검사 추가 (validationQuery)
All checks were successful
deploy-wise / deploy (push) Successful in 1m6s
db1999175a
DBCP 는 validationQuery 가 없으면 testWhileIdle 을 켜도 아무 검사도 하지 않는다.
종전에는 validationQuery 가 주석 처리돼 있고 검사 주기가 10000000ms(약 2.8시간)
이라 사실상 무검사였다. MySQL·MariaDB 의 wait_timeout 은 기본 8시간이므로 야간처럼
오래 논 뒤 첫 요청이 서버가 이미 끊은 커넥션을 받아 실패한다.

실제 발생: 2026-08-26 AccessLogDownloadJobWorker
  unexpected end of stream, read 0 bytes (socket was closed by server)

- validationQuery = SELECT 1 (종전 주석의 'select 1 from dual' 은 Oracle 표기)
- validationQueryTimeout 3초, testOnBorrow 켬
- 유휴 검사 주기 2.8시간 → 5분, 유휴 정리 기준 30분

commons-dbcp 1.3 의 BasicDataSource 에 네 setter 가 모두 존재함을 jar 로 확인했다
(없는 프로퍼티를 넣으면 컴파일은 통과하고 WAS 기동 때만 터진다).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@
All checks were successful
deploy-wise / deploy (push) Successful in 1m6s
5c7d064fbd
개인정보보호 기능개선 8차 — 관리자 화면 4종 신설

제안서·RFP 대비 화면이 없어 증빙을 낼 수 없던 항목을 채운다.

· 정기점검 리포트(SFR-004-7 #48)
  InspectionResultController 가 JSON 엔드포인트만 갖고 있어 점검결과를
  화면으로 입력·보관할 수 없었다. listView/saveResultView 를 추가하고
  기존 JSON 계약은 그대로 병존시킨다. 배치 집계값은 화면에서 수정하지
  않는다 — 감사 증빙이라 사후 변경되면 안 된다.

· 비정상접근 탐지(과업내용 다-① #49)
  탐지 5유형 로직은 있었으나 호출처가 배치뿐이었다. AbnormalAccessResultVO
  에 의심 대상 행(rows)을 실어 "누가 걸렸는지"를 요약 문자열 파싱 없이
  보여주고, 각 항목에서 접속기록 화면으로 조건을 넘겨 원본을 확인한다.
  탐지 결과를 적재하지 않는 이유는 컨트롤러 Javadoc 참조(임계치 변경 시
  과거 기준이 굳는 문제).

· 접속기록 파기이력(SER-002-2 / DBR-001-2)
  PDAL_PURGE_HISTORY 는 쌓이고 있었으나 볼 수 있는 곳이 없었다.
  "보관기간 경과분을 규정대로 파기했고 그 외 경로로는 손대지 않았다"를
  함께 입증하는 증빙이라 조회 전용으로만 연다.

· 회원정보 암호화 관리 CRUD(추가제안 Action4 / 제안서 슬라이드16)
  복호화 조회·수정·파기·신규등록 + 사유 입력 + 재인증(10분 TTL).
  목록은 항상 마스킹하고 평문은 사유를 남긴 상세 한 건에서만 보인다 —
  목록에서 평문이 뜨면 사유를 받는 의미가 없다.
  파기는 행을 지우지 않고 식별 항목만 비운다(신청·활동 기록이 참조 중).
  MBER_CRYPTO_ACCESS_HISTORY 에 처리자·일시·IP·사유·변경항목명을 남기되
  개인정보 본문은 담지 않는다.
  재인증에 로그인 2차인증을 재사용하지 않은 이유는 MberCryptoReauth
  Javadoc 참조(세션 내내 유지돼 재확인이 안 되고, OTP 미등록자가 잠긴다).

검증: JDK8 mvn compile EXIT=0, JSP 9종 Jasper 사전컴파일 오류 0건,
로컬 ROOT 반영 후 Tomcat 재기동 정상(신규 sqlMap 4건 기동시 파싱 통과),
신규 엔드포인트 6개 302→로그인 / 대조군 404 / frt/main.do 200.

DB: MBER_CRYPTO_ACCESS_HISTORY DDL + 메뉴 9100048~9100051
    (privacy.install_mysql.sql PART1·PART3 에도 병합, 재실행 안전)

후속 발견: 기존 탈퇴 구문 mberManageDAO.updateDeleteMber_S 는 개인정보를
비우면서도 이름(MBER_NM/ENC/HASH)은 지우지 않는다 — 파기 누락 소지.
신설 화면의 파기는 이름까지 파기한다. 기존 경로 보정은 별도 판단 필요.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@
제안요약서 slide 14~15 의 화면 시안을 열어보니 8차 구현과 어긋나 있었다.
시안은 좌측메뉴를 "접속기록·감사" 한 묶음으로 그리고 그 아래 4개를 두며,
이상행위를 별도 화면이 아니라 접속기록 목록의 "감사" 칸에 인라인으로 표시한다.
8차는 이 넷을 권한관리 하위에 낱개로 붙여 놓았다 — 기능은 같은데 배치가 달랐다.

· 메뉴 재편 — "접속기록 · 감사"(9100052) 그룹 신설
  ① 접속기록 조회(9100044) ② 개인정보 처리기록(9100053, 신설)
  ③ 이상행위 감사(9100049)  ④ 정기 점검 리포트(9100048)
  파기이력(9100050)은 시안에 메뉴가 없어 내렸다. 화면은 그대로 두고
  정기 점검 리포트에서 버튼으로 연다(양방향 링크). 기능을 지운 게 아니라
  노출 위치만 시안에 맞춘 것이다.
  그룹 메뉴의 PROGRM_FILE_NM 은 NOT NULL 이고 기존 그룹이 모두 센티넬 'dir'
  을 쓴다 — NULL 로 넣었다가 ERROR 1048 로 실패한 뒤 형제 행을 실측해 정정.

· 접속기록 조회 화면에 "감사" 칸 신설
  이상(처리결과 F=차단·비인가)은 붉게, 점검(정상이나 대량조회·계정공유의심·
  인증실패반복 대상 또는 업무시간 밖)은 노랗게 표시하고 해당 행을 강조한다.
  요약 줄도 시안대로 "검색결과 N건 · 보관기간 730일 · 이상행위 N건 탐지".
  판정값(auditFlag)은 저장하지 않는다 — 탐지 임계치는 정책값이라 적재해 두면
  임계치를 조정한 뒤에도 과거 기준으로 굳은 값이 남는다.
  감사 표시는 집계 5회라 기간 미지정 시 최근 30일로 잘라 계산하고,
  실패해도 목록은 그대로 보여준다(PSR-001).

· ② 개인정보 처리기록 — 조회(S)를 뺀 등록·수정·삭제만
  화면·검색조건은 ①과 공유하고 excludeSelect 조건 하나만 다르다.
  같은 표를 두 벌 관리하지 않으려는 선택이다.

검증: mvn compile EXIT=0, JSP 4종 Jasper 오류 0건, 로컬 ROOT 반영 후 재기동
정상(25.4초), 신규 processLog 포함 5개 엔드포인트 302→로그인 / 대조군 404,
통합본 privacy.install_mysql.sql PART3 [8] 병합 후 전체 재실행 오류 0건(멱등).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
제안요청서 과업내용 "다"가 요구하는 화면은 하나다 —
"ㅇ 로그 조회 및 관리 기능 구현 / - 관리자용 로그 조회 화면 개발".
제안요약서 시안도 "접속기록 조회 및 감사" 한 장만 그려져 있다.
8·9차에서 이 영역을 여러 화면으로 벌려 놓은 것을 되돌린다.

· 삭제 — 정기 점검 리포트 화면 / 이상행위 감사 화면 / 개인정보 처리기록 화면
  AbnormalAccessController, abnormal/list.jsp, inspection/list.jsp 제거.
  InspectionResultController 는 화면 엔드포인트(listView/saveResultView)만 걷어내고
  8차 이전의 JSON 계약으로 되돌렸다.
  PersonalDataAccessLogSearchVO.excludeSelect 와 그 sqlmap 조건(Mysql/Oracle 각 3곳)도
  함께 제거 — processLog 진입점이 사라져 죽은 조건이 된다.

· 기능은 남긴다 (RFP 가 '기능'으로 명시한 항목이라 지우면 요건 미충족)
  · "처리기록 검색·조회 기능 제공"       → 처리구분 필터(조회/등록/수정/삭제 토글)
  · "비정상 접근 탐지 및 기록 기능 구현"  → 목록 '감사' 칸(이상/점검) 상시 표시
  · SFR-004 "접속기록 정기점검을 지원"   → 월 1회 배치 + PDAL_INSPECTION_REPORT 적재
  · 파기이력(SER-002-2)                    → 접속기록 화면의 "파기이력" 버튼
  AbnormalAccessDetectionService 6클래스는 감사 칸이 쓰므로 그대로 둔다.

· 메뉴 — 권한관리 하위 "접속기록 조회 및 감사"(9100044, ORDR 10) 하나로 수렴
  9100048/49/50/52/53 제거. install_mysql.sql PART 3 은 [7]+[8] 을 최종형 [7] 하나로
  대체했다(이미 중간형을 적용한 DB 를 위한 정리구문 포함, 신규 설치는 무영향).

검증: mvn compile EXIT=0, JSP 3종 Jasper 오류 0건, 로컬 반영·재기동 정상.
살아있는 화면 accessLog/purgeHistory/mberCrypto 302→로그인,
삭제한 inspection/abnormal/processLog **404**(제거 증명), frt/main.do 200,
통합본 전체 재실행 오류 0건(멱등).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
목록은 페이지의 모든 행을 복호화하는데, 공용 MemberNmDecryptSupport 는
실패를 숨기지 않도록 예외를 던진다. 그 계약을 목록에 그대로 쓰면 암호문
한 건만 어긋나도 화면 전체가 500 이 된다 — 관리자 화면이 안 열리면 정작
무엇이 잘못됐는지 확인할 방법도 사라진다.

· 목록(MberCryptoAdminDAO.selectMberList)만 행 단위로 감싼다.
  실패한 행은 이름 자리에 "(복호화 실패)" 표식과 decryptFailed=true 를 남기고,
  나머지 행은 정상 표시한다. 실패를 감추는 게 아니라 어느 행인지 드러낸다.
· 단건 조회(selectMber)는 종전대로 예외를 올린다 — 상세는 값이 정확해야 한다.
  다만 컨트롤러가 받아 스택트레이스 대신 원인을 문장으로 안내하고 목록으로 되돌린다.
· 실패 표식은 마스킹 대상에서 제외한다(마스킹하면 표식이 뭉개진다).
· 화면에 실패 행을 붉게 표시하고 안내 문구를 추가했다.

이 처리는 로컬 더미 DB(백필 키 분실)에서 화면을 열기 위한 임시방편이 아니라,
운영에서 키를 교체하거나 암호문 한 건이 손상돼도 관리자 화면이 살아 있어야
한다는 요구에서 나온 것이다.

검증: mvn compile EXIT=0, JSP Jasper 오류 0건, 로컬 반영·재기동 후
mberCrypto/listView.do 302→로그인, frt/main.do 200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
제안요약서 slide15 현행진단이 "보관 기간 — 2년 이상, 기본값 730일,
관리자 설정값 미존재 → 신규 구축"으로 지목한 항목이다. 파기 배치가
자바 상수 ACCESS_LOG_RETENTION_DAYS=730 을 써서 관리자가 바꿀 수 없었다.

· ACCOUNT_MNG_POLICY.ACCESS_LOG_KEEP_DAY 신설(기본 730)
  이 표에 이미 LOGIN_HIST_KEEP_DAY 가 있어 사실상 보관·파기 정책의 단일
  창구다. 표를 새로 만들면 관리자가 보관기간을 두 화면에서 나눠 보게 된다.

· 법정 하한 730일을 세 겹으로 강제
  화면 JS / 서버 validatePolicy / 파기 배치. 화면만 막으면 요청을 직접
  만드는 경로가, 저장만 막으면 정책 행을 직접 고치는 경로가 남는다.
  배치는 정책값이 730 미만이어도 730일치를 남긴다 — 한 번 지운 접속기록은
  되돌릴 수 없어 설정 실수로 법정 보관기간을 깨는 쪽보다 더 남기는 쪽으로 기운다.
  DB CHECK 를 쓰지 않은 이유는 MariaDB/MySQL 5.7 에서 동작이 갈려 조용히
  통과할 수 있어서다.

· 접속기록 조회 화면의 "보관기간 N일" 안내도 정책값을 읽는다
  파기 배치가 실제로 쓰는 값과 달라지면 안내가 거짓말이 된다.
  정책 조회 실패 시 양쪽 모두 기본값으로 물러난다(안내 때문에 목록이 막히면 안 된다).

검증: mvn compile EXIT=0, JSP Jasper 오류 0건, DDL 적용 후
ACCESS_LOG_KEEP_DAY=730 확인, 재기동 정상, 정책화면·접속기록 302→로그인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
제안요약서 slide15 "로그 조회 및 감사 기능"의 구성요소 중 "관리자 알림".
종전에는 무결성 위반을 에러 로그로만 남겨서, 로그를 들여다보지 않으면
아무도 모르는 상태였다 — 탐지해 놓고 알리지 않으면 탐지의 의미가 없다.

· PrivacyAdminAlertService + Impl 신설
  - 무결성 위반 탐지 시(해시체인 검증 배치)
  - 이상행위 탐지 시(정기점검 배치, 0건인 달은 보내지 않음)
    매달 "0건" 메일이 오면 곧 아무도 열지 않는다.

· 채널은 메일 — 다만 문서 근거는 없다
  RFP·제안서 어디에도 알림 채널이 명시돼 있지 않다("관리자 알림" 네 글자뿐).
  ACCOUNT_MNG_POLICY.ADMIN_EMAIL 이 이미 있고 휴면 사전알림이 그 주소로
  메일을 보내고 있어 같은 경로를 재사용했다. 운영에서 메일 장애를 한 곳에서
  확인할 수 있어야 한다는 이유도 있다. SMS 는 문자발송모듈 연동규격이
  미확정이라(SFR-003 과 같은 블로커) 붙일 수 없다.
  → 채널 확정은 발주기관 협의 항목으로 남긴다.

· 메일에 개인정보 본문을 담지 않는다
  건수·기간·취급자 계정ID·요약만 넣고 상세는 화면에서 보게 한다.
  메일함이 개인정보 저장소가 되면 안 된다.

· 배치를 막지 않는다
  발송 실패·수신주소 미설정 모두 로그만 남기고 배치는 계속한다. 다만 미설정은
  WARN 으로 구분해 남긴다 — "알림이 안 왔다"와 "보낼 주소가 없다"는 다른 문제다.

검증: mvn compile EXIT=0, 로컬 반영 후 재기동 정상(빈 생성 오류 0건 —
privacyAdminAlertService/sndngMailRegistService/accountMngDAO 배선 확인),
frt/main.do 200. 실제 발송은 월 1회 배치라 이번에 실행하지 못했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
· 테스트 발송 버튼 — [계정 관리 정책] 화면의 관리자 수신 이메일 옆
  실제 알림은 월 1회 배치에서만 나가서, 수신 주소를 바꾼 관리자가 그것이
  맞는지 한 달을 기다려야 확인할 수 있었다. 같은 발송 경로(수신주소 조회 →
  메일 모듈 적재·발송)를 그대로 타는 테스트를 눌러 즉시 결과를 본다.
  경로가 다르면 테스트가 통과해도 실제 알림은 실패할 수 있으므로 반드시
  같은 경로여야 한다.

  - 성공·실패 모두 서버가 준 사유를 그대로 화면에 띄운다.
    "실패했습니다"만 뜨면 주소 미설정인지 메일서버 문제인지 구분할 수 없다.
  - 메일 모듈이 예외 없이 false 를 반환하는 경우도 실패로 잡는다
    (insertSndngMail 은 적재 후 곧바로 전송하고 실패 시 결과코드를 'F' 로 갱신한다.
     예외가 없다고 발송된 것이 아니다).
  - 상태를 바꾸지는 않지만 외부로 메일이 나가므로 POST·동일출처만 받는다.

· 발주기관 확인 항목.txt 신규
  제안요청서·제안서만으로 결정할 수 없어 발주기관 확인이 필요한 항목을
  근거 문구·현재 구현 상태·미확정 시 영향과 함께 정리했다. A(개발 완료 조건) /
  B(운영정보 필요) / C(문서에 명시 없음) / D(사업관리)로 나눴고, 확인 없이
  진행한 값은 근거를 명시해 [참고] 절에 따로 적었다.

검증: mvn compile EXIT=0, JSP Jasper 오류 0건, 로컬 반영·재기동 정상
(빈 생성 오류 0건), 정책화면 302→로그인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
웹취약점 점검(SER-004)에서 확인된 휴면계정 관련 취약점 2건 조치.

1) 인증 없는 휴면계정 해제 (CWE-306, 긴급)
   - actionDiapause.do 가 요청 파라미터 mber_id 만으로 즉시 deleteDiapause 실행 →
     아이디만 알면 제3자가 임의 계정의 휴면을 해제할 수 있었음(로컬 PoC 실증).
   - 파라미터를 신뢰하지 않고, ID/PW 인증 성공 시에만 세션에 적재되는 diapauseVO 의
     mber_id 로만 해제. 세션 부재 시 resCode=002 거부, 성공 시 1회성 소거.
   - 정상 경로 releaseDiapauseAjax(토큰 검증)와 동일 원칙. 정상 사용자는 원래도
     세션값(${sessionScope.diapauseVO.mber_id})을 보냈으므로 결과 동일(회귀 없음).
   - mbl/EgovDiapause.jsp: resCode 분기로 거부 시 재로그인 안내.

2) 예측 가능한 난수 (CWE-330, 중)
   - 휴면 활성화 메일 토큰(20자) 생성을 java.util.Random → SecureRandom 으로 교체.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbEjijyQdS54xso1d2ks3u
Spring Security 2.0.4 에 CSRF 기능이 없어 세션 동기화 토큰(Synchronizer Token)을
자체 구현. 웹취약점 점검(ZAP)의 "Absence of Anti-CSRF Tokens" 및 정적 분석에서
관리자 상태변경 요청 다수가 무방비였던 것을 조치.

- CsrfTokenInterceptor(신규): /mnt/ 상태변경 요청(POST/PUT/DELETE)에서 토큰 검증.
  이중 방어 — 토큰 없거나 불일치해도 '같은 출처'면 통과(정상 화면 회귀 방지),
  '교차 출처'만 차단(CSRF 공격 차단). 토큰은 SecureRandom 48자 hex.
- egov-com-servlet.xml: 인터셉터 리스트 등록 + bean 정의(enforce=true, /mnt/ 한정).
- mnt/include/head.jsp: 토큰 meta + 전역 스크립트로 모든 폼(hidden)·AJAX(X-CSRF-TOKEN
  헤더)에 자동 주입. 167개 관리자 화면 공통 적용, 개별 화면 수정 없음.

검증: cross-origin+토큰없음 → 403 차단, same-origin(토큰유무 무관) → 통과(회귀 없음).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbEjijyQdS54xso1d2ks3u
CWE-330(취약한 난수). BizEntrprsFrtManageController.randomValue 가 임시비밀번호를
Math.random()/java.util.Random 으로 생성 → 시드 예측 가능. 암호학적 안전 난수
(SecureRandom)로 교체하고 문자 분포는 정수 범위(nextInt)로 균등 생성.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbEjijyQdS54xso1d2ks3u
[보안/SER-004] SMS DB 자격증명 하드코딩 제거 + 보안 응답 헤더
Some checks failed
deploy-wise / deploy (push) Successful in 1m6s
deploy-dev / deploy (push) Has been cancelled
6b57520afd
1) 계정정보 하드코딩 (CWE-798)
   - SmsBasicDBUtil 의 JDBC URL/USER/PASSWORD 소스 하드코딩 제거 →
     EgovProperties(Globals.sms.jdbc.url/username/password)로 외부화.
   ※ globals-*.properties 는 .gitignore 대상(자격증명 미추적)이므로, 배포 환경마다
     아래 키를 별도 설정해야 한다:
       Globals.sms.jdbc.url / .username / .password

2) 보안 응답 헤더 (CWE-693 등)
   - web.xml 에 Tomcat HttpHeaderSecurityFilter 추가.
     X-Frame-Options: SAMEORIGIN(관리자 iframe 사용 → DENY 아님),
     X-Content-Type-Options: nosniff. HSTS 는 HTTP 환경 오작동 방지로 off(운영 HTTPS 시 on).
   - 쿠키 HttpOnly 는 Tomcat 기본 적용 확인(추가 조치 불요).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbEjijyQdS54xso1d2ks3u
Some checks failed
deploy-wise / deploy (push) Successful in 1m6s
deploy-dev / deploy (push) Has been cancelled
This branch is already included in the target branch. There is nothing to merge.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature/privacy-access-control-policy:feature/privacy-access-control-policy
git checkout feature/privacy-access-control-policy
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: somang4819/arko_renual#1
No description provided.