4개 도메인 병렬 분석 결과 + 통합 요약 + 시나리오 중심 정리.
총 25개 버그 (CRITICAL 3 / HIGH 11 / MEDIUM 7 / LOW 4) 식별.
실제 수정은 별도 커밋 (commit 68c1cb5b).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.7 KiB
부서관리 백엔드 버그 헌팅 (2026-05-08)
대상:
backend-spring/src/main/java/com/erp/controller/DepartmentController.javabackend-spring/src/main/java/com/erp/service/DepartmentService.javabackend-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-111 — MAX(...)+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 — 시나리오:
- 사용자가 deptA 소속 (IS_PRIMARY=TRUE)
- 클라이언트가 사용자가 소속되지 않은 deptB 로 PUT 호출
clearUserPrimaryDept→ 사용자의 모든 USER_DEPT 행 IS_PRIMARY=FALSE (deptA primary 제거)setUserPrimaryDept(WHERE USER_ID=? AND DEPT_CODE=deptB) → 0 rows affected- 결과: 어떤 부서도 primary 가 아닌 상태로 남음 (invariant 깨짐)
Service 진입 시 selectExistingMember 선검증 + 0 row affected 시 예외/롤백 필수.
11. canAccessDept — 글로벌 부서(*) read 허용 ⚠️ MEDIUM
DepartmentController.java:361-367 — userCompanyCode.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, 133 — trimString 은 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-355 — isSuperAdmin(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-288 — params.put("search", "%" + search + "%"). 사용자 입력의 %/_ 가 와일드카드. _ 단독 검색 시 모든 사용자 매칭. 데이터 누출은 아니나 enumeration 가능.
C. searchUsers 컨트롤러 권한 검증 누락 ❌ CRITICAL — TENANT BREACH
DepartmentController.java:234-245 — userCompanyCode 가드 없음. 임의 사용자가 다른 회사 코드를 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 |
즉시 수정 권장
searchUsers컨트롤러 권한/회사 가드 추가setPrimaryDept선검증 + 0 rows 시 예외create/updateDepartmentparent 회사 격리·존재·미삭제 검증update/restoreDepartment에selectDuplicateDeptName추가- 글로벌 부서 (
*) write 작업 SUPER_ADMIN 전용 가드 dept_code형식 위반 시 silent fallback 대신 400