시큐어 코딩
02 보안기능
- 적절한 인증 없는 중요기능 허용
- 적절한 인증과정없이 중요정보를 열람 또는 변경하지 않도록 해야한다.
- 클라이언트의 보안 검사를 우회하여 서버에 접근하지 못하도록 설계하고 중요한 정보가 있는 페이지는 재인증을 적용한다.
- 검증된 라이브러리나 프레임워크(OpenSSL, ESAPI)를 사용한다.
- 예시
-
// 1. 로그인한 사용자를 불러온다. String userId = (String) session.getAttribute("userId"); String passwd = request.getParameter("oldUserPw"); // 2. 회원정보를 실제 수정하는 사용자와 로그인 사용자와 동일한지 확인한다. String requestUser = memberModel.getUserId(); if (userId != null && requestUser != null && !userId.equals(requestUser)) { ...코드생략... // 3. 동일한 경우에만 회원정보를 수정해야 안전하다. if (service.modifyMember(memberModel)) { ...코드생략...
-
- 부적절한 인가
- 프로그램 실행 경로에 대해서 접근제어를 검사하지 않거나 불완전하게 검사하는 경우 공격자가 접근 가능한 실행경로로 정보를 유출하지 않도록 해야한다.
- 공격 노출면을 최소화하고 사용자 건한에 따른 ACL(Access Control List)을 관리한다.
- JAAS Authorization 프레임워크나 OWASP ESAPI Access Control 등을 인증 프레임워크로 사용 가능하다.
- 예시
-
... // 세션에 저장된 사용자 정보를 얻어온다. User user= (User)session.getAttribute("user"); // 사용자정보에서 해당 사용자가 delete 작업의 권한이 있는지 확인한 뒤 삭제 작업을 수행한다. if (action != null && action.equals("delete") && checkAccessControlList(user,action)) { boardDao.delete(contenId); }
-
- 중요한 자원에 대한 잘못된 권한 설정
- 보안관련 자원에 대한 권한을 의도하지 않게 허가할 경우, 권한을 갖지 않은 사용자가 해당 자원을 사용하지 않도록 해야한다.
- 설정파일과 같은 중요한 자원을 사용하는 경우 허가받지 않은 사용자가 중요한 자원에 접근 가능한지 검사한다.
- 예시
-
File file = new File("/home/setup/system.ini"); // 소유자에게 실행 권한 금지 file.setExecutable(false); // 소유자에게 읽기 권한 허용 file.setReadable(true); // 소유자에게 쓰기 권한 금지 file.setWritable(false);
-
- 취약한 암호화 알고리즘 사용
- 정보보호에 취약한 암호화 알고리즘을 사용하면 안된다.
- 학계 및 업계에서 이미 검증된 표준화된 알고리즘을 사용한다. (3DES, AWS, SEED)
- 예시
- var des = new AesCryptoServiceProvider();
- 암호화되지 않은 중요 정보
- 중요 정보가 포함된 데이터를 평문으로 송수신 또는 저장할 때 인가되지 않은 사용자에게 정보가 노출되지 않도록 해야한다.
- 중요정보를 통신 채널로 전송하거나 저장할 때 반드시 암호화 과정을 거쳐야 한다.
- SSL 또는 HTTPS 등과 같은 암호 채널을 사용한다.
- 브라우저 쿠키에 중요 정보를 저장하는 경우 쿠키객체에 보안속성을 반드시 설정하여 중요정보의 노출을 방지한다.
- 중요정보를 읽거나 쓸 경우에 권한인증 등으로 적합한 사용자가 접근하도록 해야한다.
- 예시
-
String pwd = request.getParameter("pwd"); // 패스워드를 솔트값을 포함하여 SHA-256 해쉬로 변경하여 안전하게 저장한다. MessageDigest md = MessageDigest.getInstance("SHA-256"); md.reset(); md.update(salt);
-
- 하드코드된 중요정보
- 프로그램 코드 내부에 하드코드된 패스워드 또는 암호화키를 포함하여 내부 인증에 사용하거나 암호화 수행 시 중요정보가 유출될 수 있다.
- 패스워드는 암호화하여 별도 파일에 저장하여 사용한다.
- 중요 정보를 암호화하면 상수가 아닌 암호화 키를 사용하도록 하고, 소스코드 내부에 상수형태의 암호화 키를 저장해서 사용하지 않도록 한다.
- 예시
-
// 암호화된 패스워드를 프로퍼티에서 읽어들여 복호화해서 사용해야한다. String PASS = props.getProperty("EncryptedPswd"); byte[] decryptedPswd = cipher.doFinal(PASS.getBytes()); PASS = new String(decryptedPswd); con = DriverManager.getConnection(URL, USER, PASS);
-
- 충분하지 않은 키 길이 사용
- 길이가 짧은 키를 사용하는 것은 암호화 알고리즘을 취약하게 만든ㄷ.
- RSA 알고리즘은 적어도 2,048 비트 이상, 대칭암호화 알고리즘은 128이상의 길이를 가진 키와 함께 사용해야 한다.
- 예시
- final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
- keyGen.initialize(2048);
- 적절하지 않은 난수값 사용
- 예측 가능한 난수를 사용하는 것은 보안약점을 유발하기 때문에 사용하지 말아야 한다.
- 난수 값을 결정하는 시드 값은 현재시간을 기반으로 조합하여 매번 변경되는 시드 값을 사용해야 한다.
- 예시
-
// setSeed로 매번 변경되는 시드값을 설정 하거나, 기본값인 현재 시간 기반으로 매번 변경되는 시드값을 사용하도록 한다. Random random = new Random(); return random.nextInt(maxValue); // 보안결정을 위한 난수로는 예측이 거의 불가능하게 암호학적으로 보호된 SecureRandom을 사용한다. SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); MessageDigest digest = MessageDigest.getInstance("SHA-256"); secureRandom.setSeed(secureRandom.generateSeed(128)); String authKey = new String(digest.digest((secureRandom.nextLong() + "").getBytes()));
-
- 취약한 비밀번호 허용
- 사용자에게 강한 비밀번호 조합 규칙을 요구하지 않으면 사용자 계정이 취약하게 된다.
- 비밀번호 생성 시 강한 조건 검증을 수행한다. (숫자, 영문자, 특수문자 혼합하여 정해진 자릿수)
- 비밀번호를 주기적으로 변경하도록 해야 한다.
- 예시
-
// 비밀번호에 자릿수, 특수문자 포함 여부 등의 복잡도를 체크하고 등록하게 한다. Pattern pattern = Pattern.compile("((?=.*[a-zA-Z])(?=.*[0-9@#$%]). {9, })"); Matcher matcher = pattern.matcher(pass); if (!matcher.matches()) { return "비밀번호 조합규칙 오류"; } UserVo userVO = new UserVo(id, pass); String result = registerDAO.register(userVO);
-
- 부적절한 전자서명 확인
- 전자서명이란 서명자의 신원 확인 및 서명된 파일의 무결성을 보장할 수 있는 디지털 정보이다.
- 전자서명을 검증하지 않거나 검증절차가 부적절하면 위변조된 파일로 악성코드에 감염될 수 있으므로 위변조 여부를 판별하고 사용해야 한다.
- 부적절한 인증서 유효성 검증
- 인증서 확인 절차를 거쳐서 신뢰할 수 없는 호스트에서 생성된 데이터를 수신하지 않도록 해야한다.
- 인증서 사용 전 인증서의 유효성을 확인하고, 안전한 암호화 알고리즘 사용여부 확인으로 검증 절차를 거쳐야한다.
- 예시
-
/* 검증하려는 호스트 인증서(toVerify)와 CA인증서(signing Cert)의 DN(Distinguished Name)이 일치하는지 여부를 확인한다.*/ if (!toVerify.getIssuerDN().equals(signingCert.getSubjectDN())) return false; try { // 호스트 인증서가 CA인증서로 서명 되었는지 확인한다. toVerify.verify(signingCert.getPublicKey()); // 호스트 인증서가 유효기간이 만료되었는지 확인한다. toVerify.checkValidity();
-
- 사용자 하드디스크에 저장되는 쿠키를 통한 정보노출
- 대부분 쿠키는 메모리에 상주하며 브라우조 종료시 사라진다. 하지만 브라우저 세션에 관계없이 지속적으로 저장되도록 설정할 수 있으며 다음 브라우저 세션 시작시 메모리에 로드된다.
- 이렇게 개인정보, 인증정보 등이 영속적인 쿠키에 저장되면 시스템을 취약하게 만든다.
- 쿠키의 만료시간은 세션이 지속되는 시간을 고려하여 최소한으로 설정하고, 영속적인 쿠키에는 사용자 권한 등급이나 세션ID같이 중요정보가 포함되지 않도록 한다.
- 주석문 안에 포함된 시스템 주요정보
- 패스워드를 주석문에 넣어두면 시스템 보안이 훼손될 수 있다.
- 개발자가 주석에 패스워드를 적은 후, 소프트웨어가 완성된 후에는 제거하는 것이 어렵다.
- 주석에는 ID, 패스워드 등 보안과 관련된 내용을 기입하지 않는다.
- 반복된 인증시도 제한 기능 부재
- 일정시간 내에 여러 번의 인증을 시도하면 계정잠금 또는 추가인증 방법 등을 거쳐서 시스템에 접근하도록 해야한다.
- 예시
-
private static final int MAX_ATTEMPTS = 5; int count = 0; try { socket = new Socket(SERVER_IP, SERVER_PORT); // 인증 실패 및 시도 횟수에 제한을 둔다. while (result == FAIL && count < MAX_ATTEMPTS) { ... result = verifyUser(username, password); count++; }
-
Spring Security
- 스프링 시큐리티 설치
- build.gradle- dependencies 추가
- implementation 'org.springframework.boot:spring-boot-starter-security’
- implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6’
- localhost:8080 으로 접속하여 로그인 화면이 나오는지 확인
- 메인 Application과 같은 위치에 SecurityConfig.java 생성
-
package com.mysite.sbb; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 로그인하지 않아도 모든 페이지에 접근할 수 있게 한다. .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests .requestMatchers(new AntPathRequestMatcher("/**")).permitAll()) // h2 DB에 검증없어도 접근이 가능하도록 한다. .csrf((csrf) -> csrf .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**"))) // h2 DB의 프레임(레이아웃)이 깨져보이지 않도록 한다. .headers((headers) -> headers .addHeaderWriter(new XFrameOptionsHeaderWriter( XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))) ; return http.build(); } }
-
- build.gradle- dependencies 추가
- 스프링 시큐리티 관련 어노테이션
- @Configuration
- 해당 파일이 스프링의 환경 설정 파일임을 의미하는 어노테이션
- 여기서는 스프링 시큐리티를 설정하기 위해 사용
- @EnableWebSecurity
- 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 어노테이션
- 스프링 시큐리티 활성화하는 역할
- @Bean
- 스프링 시큐리티의 세부 설정을 위한 어노테이션
- @Configuration
- 회원가입 기능
- 회원 엔티티 생성
- SecurityConfig.java에 비밀번호 암호화하여 저장하도록 하는 메서드 추가 (PasswordEncoder , BCryptPasswordEncoder 이용)
-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig { ...코드생략... // 비밀번호 암호화하여 저장하는 메소드 @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
-
- 회원 Repository 생성
- 회원 Service 생성
- 예시
-
package com.mysite.sbb.user; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; // 회원가입 기능 public SiteUser create(String username, String email, String password) { SiteUser user = new SiteUser(); user.setUsername(username); user.setEmail(email); // 스프링 시큐리티의 PasswordEncoder로 비밀번호 암호화 후 저장 user.setPassword(passwordEncoder.encode(password)); this.userRepository.save(user); return user; } }
-
- 예시
- 회원가입 DTO 생성
- 회원 Controller 생성
- 예시
-
package com.mysite.sbb.user; import jakarta.validation.Valid; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.dao.DataIntegrityViolationException; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Controller @RequestMapping("/user") public class UserController { private final UserService userService; @GetMapping("/signup") public String signup(UserCreateForm userCreateForm) { return "signup_form"; } // 회원가입 기능 @PostMapping("/signup") public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return "signup_form"; } // 비밀번호와 비밀번호 확인 값 일치 확인 if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) { bindingResult.rejectValue("password2", "passwordInCorrect", "2개의 패스워드가 일치하지 않습니다."); return "signup_form"; } // 중복 회원 방지 try { userService.create(userCreateForm.getUsername(), userCreateForm.getEmail(), userCreateForm.getPassword1()); }catch(DataIntegrityViolationException e) { e.printStackTrace(); bindingResult.reject("signupFailed", "이미 등록된 사용자입니다."); return "signup_form"; }catch(Exception e) { e.printStackTrace(); bindingResult.reject("signupFailed", e.getMessage()); return "signup_form"; } return "redirect:/"; } }
-
- 예시
- 회원가입 폼 html 생성
- 예시
-
<html layout:decorate="~{layout}"> <div layout:fragment="content" class="container my-3"> <div class="my-3 border-bottom"> <div> <h4>회원가입</h4> </div> </div> <form th:action="@{/user/signup}" th:object="${userCreateForm}" method="post"> <div th:replace="~{form_errors :: formErrorsFragment}"></div> <div class="mb-3"> <label for="username" class="form-label">사용자ID</label> <input type="text" th:field="*{username}" class="form-control"> </div> <div class="mb-3"> <label for="password1" class="form-label">비밀번호</label> <input type="password" th:field="*{password1}" class="form-control"> </div> <div class="mb-3"> <label for="password2" class="form-label">비밀번호 확인</label> <input type="password" th:field="*{password2}" class="form-control"> </div> <div class="mb-3"> <label for="email" class="form-label">이메일</label> <input type="email" th:field="*{email}" class="form-control"> </div> <button type="submit" class="btn btn-primary">회원가입</button> </form> </div> </html>
-
- 예시
728x90
'교육 (Today I Learned) > Hanaro' 카테고리의 다른 글
[Hanaro] 71일차 ~ 78일차 / 키오스크 프로젝트 완료 (회고) (2) | 2024.05.11 |
---|---|
[Hanaro] 69일차 / Spring Boot (Security), 시큐어 코딩 (시간 및 상태, 에러 처리) (0) | 2024.04.25 |
[Hanaro] 67일차 / 시큐어 코딩 (입력데이터 검증), Spring Boot (네비게이션 바, 페이징) (0) | 2024.04.23 |
[Hanaro] 66일차 / 백엔드 실습 과제 (0) | 2024.04.21 |
[Hanaro] 65일차 / 백엔드 실습 과제 (0) | 2024.04.21 |