Files
invyone/notes/johngreen/2026-05-08-부서관리-버그헌팅-backend.md
T
johngreen 9c658ffd36 docs(notes): 부서관리 버그 헌팅 분석 리포트 (frontend/backend/sql/ux)
4개 도메인 병렬 분석 결과 + 통합 요약 + 시나리오 중심 정리.
총 25개 버그 (CRITICAL 3 / HIGH 11 / MEDIUM 7 / LOW 4) 식별.
실제 수정은 별도 커밋 (commit 68c1cb5b).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:08:23 +09:00

8.7 KiB

부서관리 백엔드 버그 헌팅 (2026-05-08)

대상:

  • backend-spring/src/main/java/com/erp/controller/DepartmentController.java
  • backend-spring/src/main/java/com/erp/service/DepartmentService.java
  • backend-spring/src/main/resources/mapper/department.xml (참고)

1. createDepartment dept_code silent fallback HIGH

DepartmentService.java:85-99 — 사용자가 명시한 dept_code 가 정규식 ^[A-Za-z0-9_]+$ 위반 시 예외 없이 자동 코드 생성으로 폴백. 사용자는 201 응답을 받지만 응답의 dept_code 가 요청과 다름 (silent override). IllegalArgumentException("부서 코드 형식이 올바르지 않습니다") 던지는 게 맞음.

2. createDepartment parent_dept_code 검증 누락 CRITICAL — cross-tenant

DepartmentService.java:107, DepartmentController.java:80-82 — parent 가 (a) 존재, (b) 같은 회사, (c) deleted 아님 검증 전혀 없음. SUPER_ADMIN 또는 controller 우회 경로로 cross-tenant 부모 지정 가능. FK 가 같은 DB 내 다른 회사 부서를 참조 가능.

3. updateDepartment 부서명 중복 검증 누락 HIGH

DepartmentService.java:131-170 — create 는 selectDuplicateDeptName 체크하지만 update 에는 없음. UNIQUE 제약도 mapper 에 없음. 회사 내 동일 이름의 active 부서 두 개 공존 가능.

4. verifyParentCycle 회사 격리 미검증 HIGH

DepartmentService.java:251-271 — cycle 만 체크. newParent.company_code 와 deptCode.company_code 비교 없음. update 흐름에서 #2 와 동일 결함 재현.

5. selectNextDeptNumber race condition MEDIUM

DepartmentService.java:96-99, department.xml:108-111MAX(...)+1 비원자적. 두 요청 동시 진입 시 같은 next_number 읽음 → 두 번째 INSERT PK 위반으로 500. 컨트롤러 catch 절은 DuplicateDeptNameException/IllegalArgumentException 만 잡으므로 raw DataIntegrityViolationException 누출.

6. delete-restore trap (활성 자식만 체크) ⚠️ MEDIUM

DepartmentService.java:181-202 (include_deleted=false 로 자식 카운트) — 활성 자식 0이면 부모 soft-delete OK. 자식 단독 복구 시도 시 restoreDepartment 가 부모 deleted 검사로 차단(PARENT_DELETED) → 부모 먼저 복구 필요. 영구 trap 은 아니지만 UX 혼선.

7. restoreDepartment 부서명 충돌 미검증 HIGH

DepartmentService.java:210-239 — 시나리오: A 부서 삭제 → 같은 이름 B 부서 생성 (create 는 active 만 검사하므로 통과) → A 부서 복구 → 동일 이름 active 두 부서 공존. 100% 재현 가능.

8. restoreDepartment 부모 1단계 검증

DepartmentService.java:220-228 — 시스템 invariant 상 부모 active 면 조부모도 active 보장됨 → 1단계로 충분. 외부 직접 SQL 조작 시나리오는 막지 못하지만 OK.

9. addDeptMember dead code LOW

DepartmentController.java:271-272:

Object userIdObj = body.get("user_id");
if (userIdObj == null) userIdObj = body.get("user_id");  // 동일 키

camelCase fallback 의도였던 것이 작성 실수로 같은 snake_case 가 됨. → body.get("userId") 로 수정.

10. setPrimaryDept 멤버십 미검증 → DATA CORRUPTION CRITICAL

DepartmentService.java:356-370 — 시나리오:

  1. 사용자가 deptA 소속 (IS_PRIMARY=TRUE)
  2. 클라이언트가 사용자가 소속되지 않은 deptB 로 PUT 호출
  3. clearUserPrimaryDept → 사용자의 모든 USER_DEPT 행 IS_PRIMARY=FALSE (deptA primary 제거)
  4. setUserPrimaryDept (WHERE USER_ID=? AND DEPT_CODE=deptB) → 0 rows affected
  5. 결과: 어떤 부서도 primary 가 아닌 상태로 남음 (invariant 깨짐)

Service 진입 시 selectExistingMember 선검증 + 0 row affected 시 예외/롤백 필수.

11. canAccessDept — 글로벌 부서(*) read 허용 ⚠️ MEDIUM

DepartmentController.java:361-367userCompanyCode.equals(deptCompanyCode) || "*".equals(deptCompanyCode). 일반 USER 도 글로벌 부서 GET 가능 (의도된 듯). 단 read/write 분리 필요.

12. deleteDepartment — COMPANY_ADMIN 의 글로벌 부서 삭제 HIGH

DepartmentController.java:135-165 — 임의 테넌트 COMPANY_ADMIN 이 글로벌 부서 ('*') 를 DELETE 호출 → isAdmin(role) 통과 + canAccessDept 통과 (글로벌 매칭) → 삭제 진행. 글로벌 자원 write 는 SUPER_ADMIN 전용 가드 필요.

13. trimString 일관성 MEDIUM

DepartmentService.java:62, 85, 133trimString 은 dept_name, requestedCode 에만 적용. parent_dept_code, dept_type, short_name, address1/2, zipcode 등은 nullIfBlank 만 거치는데 이건 빈 문자열만 null 로 바꿈 — 선행/후행 공백 보존됨. parent_dept_code=" DEPT_3 " 입력 시 DB 에 공백 포함 코드 저장. → nullIfBlank 자체를 trim+blank→null 한 번에 처리하도록 수정.

14. removeDeptMember 트랜잭션 race ⚠️ LOW

DepartmentService.java:324-354 — 메서드 전체 @Transactional 이라 데이터 일관성 OK. 단 selectFirstUserDept 후 다른 트랜잭션이 그 row 도 삭제하는 race 시 setUserPrimaryDept 0 row affected, 예외 없음 → primary 부재 commit. 권장: row count 0 이면 재조회 또는 단일 SQL UPDATE ... WHERE USER_ID=? AND DEPT_CODE=(SELECT ...) 합치기.

15. SUPER_ADMIN 식별 — company_code = '*' 의미 충돌 ⚠️ MEDIUM

DepartmentController.java:353-355isSuperAdmin(companyCodeOrRole)* 또는 SUPER_ADMIN 둘 다 true. 일반 user 의 company_code 가 우연히 * 로 저장되면 super 권한 부여. provisioning 레이어에서 company_code='*' 차단 필수. 권장: role 만으로 super 판별, company_code 와 분리.


추가 발견

A. createDepartment 중복명 race HIGH

selectDuplicateDeptName → INSERT 사이 동시 요청 race. DB UNIQUE 제약 없으면 중복 이름 공존 가능. (department.xml 에서 unique 제약 없음.)

B. searchUsers SQL LIKE wildcard ⚠️ MEDIUM

DepartmentService.java:283-288params.put("search", "%" + search + "%"). 사용자 입력의 %/_ 가 와일드카드. _ 단독 검색 시 모든 사용자 매칭. 데이터 누출은 아니나 enumeration 가능.

C. searchUsers 컨트롤러 권한 검증 누락 CRITICAL — TENANT BREACH

DepartmentController.java:234-245userCompanyCode 가드 없음. 임의 사용자가 다른 회사 코드를 path 에 넣으면 그 회사 사용자 목록 검색 가능. 다른 모든 메서드엔 가드 있는데 여기만 누락.

D. searchUsers role 검증 누락 MEDIUM

admin 검사 없음. 일반 USER 가 회사 내 모든 사용자 (이름/아이디) 자유 검색 → 사용자 enumeration.

E. getDeptMembers — soft-deleted 부서 멤버 노출 ⚠️ LOW/MEDIUM

getDepartmentIncludingDeleted 로 검증만 하고 deleted 부서 members 그대로 반환. 의도된 사양인지 확인 필요.

F. ClassCastException 위험 ⚠️ LOW

DepartmentService.java:81(String) company.get("company_name"). 컬럼 타입 변경 시 CCE. Objects.toString(...) 권장.

G. setPrimaryDept 비원자성 ⚠️ LOW

DepartmentService.java:356-370 — clear + set 두 SQL. 단일 UPDATE 합치기 가능: UPDATE USER_DEPT SET IS_PRIMARY = (DEPT_CODE = #{dept_code}) WHERE USER_ID = #{user_id}.

H. nextNumber unboxing NPE ⚠️ LOW

DepartmentService.java:97((Number) codeResult.get("next_number")).longValue(). 항상 non-null 보장이지만 NPE 가드 권장.

I. 로그 PII 평문 ⚠️ LOW

DepartmentService.java:124, 200, 237, 321, 348, 352, 369 — userId 평문 로깅. PIPA/GDPR 환경 고려.

J. selectNextDeptNumber 회사 격리

DB-per-tenant 아키텍처상 OK. 글로벌 * 회사도 같은 시퀀스 공유.


우선순위 정리

등급 항목
CRITICAL #2 parent cross-tenant 무검증 / #10 setPrimaryDept data corruption / C searchUsers tenant breach
HIGH #1 dept_code silent fallback / #3 update 중복명 / #4 verifyParentCycle 회사 / #7 restore 중복명 / #12 글로벌 삭제 / A create race / D searchUsers role
MEDIUM #5 nextNumber race / #6 delete-restore trap / #11 글로벌 write 정책 / #13 trim 일관성 / #15 * 의미 / B LIKE wildcard / E deleted member 노출
LOW #9 dead code / #14 removeMember race / F-I

즉시 수정 권장

  1. searchUsers 컨트롤러 권한/회사 가드 추가
  2. setPrimaryDept 선검증 + 0 rows 시 예외
  3. create/updateDepartment parent 회사 격리·존재·미삭제 검증
  4. update/restoreDepartmentselectDuplicateDeptName 추가
  5. 글로벌 부서 (*) write 작업 SUPER_ADMIN 전용 가드
  6. dept_code 형식 위반 시 silent fallback 대신 400