교육 (Today I Learned)/Hanaro

[Hanaro] 68일차 / 시큐어 코딩 (보안 기능), Spring Boot(Security)

Bay Im 2024. 4. 25. 09:30

시큐어 코딩

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();
            }
        }
  • 스프링 시큐리티 관련 어노테이션
    • @Configuration
      • 해당 파일이 스프링의 환경 설정 파일임을 의미하는 어노테이션
      • 여기서는 스프링 시큐리티를 설정하기 위해 사용
    • @EnableWebSecurity
      • 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 어노테이션
      • 스프링 시큐리티 활성화하는 역할
    • @Bean
      • 스프링 시큐리티의 세부 설정을 위한 어노테이션
  • 회원가입 기능
    • 회원 엔티티 생성
    • 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