kccf/.claude/skills/kccf-security-check/SKILL.md
ythong 80b5ed1da4 Initial commit - KCCF 문화원연합회 누리집
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 11:38:15 +09:00

125 lines
3.4 KiB
Markdown

---
name: kccf-security-check
description: "KCCF 코드의 보안 취약점을 검토하고 수정한다. 인증/권한 누락, XSS, SQL 인젝션, 세션 취약점 점검 시 사용한다. '보안 검토', '취약점 확인', '권한 검사', 코드 리뷰 요청 시 적용."
---
# KCCF 보안 검토 스킬
## 검토 우선순위
1. **인증 누락** (Critical) — 인증 없이 데이터 접근 가능
2. **XSS** (High) — 사용자 입력을 HTML에 직접 출력
3. **SQL 인젝션** (High) — MyBatis `${}` 오용
4. **권한 우회** (High) — 권한 확인 없이 쓰기/삭제
5. **세션 고정** (Medium) — 로그인 후 세션 재생성 미처리
## 체크리스트
### 인증 검증 (Controller)
```java
// ✅ 올바른 패턴 — /sec/ URL에 반드시 존재해야 함
UserVO userVO = (UserVO)session.getAttribute("user");
if(userVO == null || StringUtils.isAllBlank(userVO.getUserId())) {
return "redirect:/login.do";
}
// ❌ 잘못된 패턴 — 세션 체크 없이 데이터 접근
public String deleteData(VO vo) {
service.delete(vo); // 누가 호출했는지 모름
}
```
확인 항목:
- `/sec/` 접두어 URL을 처리하는 모든 메서드에 세션 검사 존재?
- Ajax 엔드포인트(`*Ajax.do`)에도 세션 검사 또는 Spring Security 설정?
- 삭제/등록/수정 메서드에 세션 검사?
### XSS 방지 (JSP)
```jsp
<%-- ✅ 안전 --%>
<c:out value="${userInput}"/>
${fn:escapeXml(userInput)}
<%-- ❌ 위험 --%>
${userInput}
<%= request.getParameter("name") %>
```
예외 허용 케이스:
- CKEditor HTML 편집기 출력 (서버 측 sanitize 전제)
- 개발자가 명시적으로 HTML을 허용한 필드
### SQL 인젝션 (Mapper XML)
```xml
<!-- ✅ 안전: PreparedStatement -->
WHERE NM = #{searchNm}
<!-- ❌ 위험: String substitution -->
WHERE NM = '${searchNm}'
ORDER BY ${sortColumn} ← ORDER BY 동적 처리 주의
```
ORDER BY 동적 처리 시 화이트리스트:
```java
// Controller 또는 Service에서
String[] allowedColumns = {"REG_DTTM", "NM", "CODE"};
if (!Arrays.asList(allowedColumns).contains(sortColumn)) {
sortColumn = "REG_DTTM"; // 기본값
}
```
### 권한 검증
```java
// 쓰기 권한 확인
searchVO.setCanWrite(AuthUtil.canWrite(getUserId(), getSiteSeCode(), getSelMenuId(), ""));
// JSP에서 확인
<c:if test="${searchVO.canWrite}">
<button onclick="fnDelete()">삭제</button>
</c:if>
```
주의: JSP 버튼 숨기기만으로는 불충분 — Controller에서도 권한 재확인 필요.
### 파일 업로드
```java
// ✅ 허용 확장자 검증
String[] allowedExt = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".hwp", ".xlsx"};
String ext = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
if (!Arrays.asList(allowedExt).contains(ext)) {
throw new IllegalArgumentException("허용되지 않는 파일 형식");
}
// ❌ 위험: 확장자 검증 없이 저장
String savedPath = uploadDir + originalFileName;
```
## 수정 우선순위 결정
| 등급 | 기준 | 처리 |
|------|------|------|
| Critical | 인증 없이 데이터 변경 가능 | 즉시 수정 |
| High | XSS, SQL인젝션, 권한 우회 | 즉시 수정 |
| Medium | 불필요한 데이터 노출, 로깅 누락 | 수정 권고 |
| Low | 코드 스타일, 예외처리 개선 | 권고 |
## 출력 형식
```markdown
## 보안 검토 결과
### Critical
- [파일:라인] 문제 설명 → 수정 방법
### High
- ...
### 통과 항목
- ...
```