포스트

타 사용자의 개인정보 탈취 가능 여부 확인 회고


해당 이슈가 발생한 이유

  • 기존에 협업사 측에서 회원의 정보를 보내줄 때 고정된 5~6자리 대소문자의 UID를 보냄
  • UID를 타 사용자의 것으로 변조할 경우 회원 정보 탈취 위험

첫 번째 해결 방법

  • UID의 길이가 짧고 바로 회원 정보를 탈취할 수 있어서 accessToken방식으로 받도록 수정
  • accessTokenheader에서 Bearer 로 받도록 한다.


간단하게 해결할 생각

1
2
3
4
5
// 기존에 UID를 받아서 처리하던 방식
@Valid @RequestBody LoginDTO data

// header에서 token을 받아서 처리하도록 수정  
@RequestHeader("Authorization") String authorizationHeader
  • headertoken을 받고


예시 코드

1
2
3
4
5
6
7
        // Bearer 중복 확인
        String accessToken = bearerToken.startsWith("Bearer ") ? bearerToken.substring(7) : bearerToken;

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("Authorization", "Bearer " + accessToken);
        headers.set("Key", apiKey);
  • Bearer 중복 체크를 하고 headersset해 주는 방식으로 처리 하려고 했다.


프론트 이슈 대응

  • 예를 들어 앱을 업데이트 하지 않고 사용하는 사용자가 있다면 해당 방식으로 기존에 사용하던 api를 사용할 수 없게 된다.
  • 즉, 기존에 사용하는 기능을 유지하면서 token방식을 추가 하거나 api를 나눠서 작업해야 했다.

나의 선택

  • 처음에는 하나의 api에서 분기 처리하려 했으나, UID방식은 결국에는 사라져야 하는 방식이기 때문에 유지보수를 생각해서 api를 따로 나눠서 작업하고, 후에 UID방식을 삭제할 수 있도록 작업 하려고 생각했다.
  • 하지만 프론트 쪽에서는 GET방식의 쿼리 스트링으로만 값을 가져올 수 있었다.
  • 그로 인해 header에서 token을 받아 오는 것이 아닌 LoginDTO에서 UIDtoken 값을 받을 수 있게 수정하기로 했다.


1
2
3
4
5
6
7
8
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginDTO {

    private String uid;
    private String token;
}
  • api를 기존에 사용하는 것과 동일하게 사용하며 위와 같이 token을 추가해서 처리하기로 결정


협업사 측의 요구사항 변경 및 추가

  • 협업사 측 보안팀에서 token형태 그대로 쿼리 스트링으로 보내게 되면 token으로 보내더라도 보안문제는 그대로 발생하기 때문에 동일한 대칭키를 가지고 aes256으로 암호화하는 방식으로 다시 진행하게 되었다.

암호화 관련 소통 부족

  • 입사하고 일주일 만에 협업을 바로 시작하게 되면서 어떤 식으로 요청을 하고 협업해야 하는지 적응하며 작업을 진행했다.
  • 대칭키는 개발서버 / 운영서버에 맞는 키를 전달 받았고 properties에 세팅하여 서버에 맞게 꺼내 쓰도록 했다.
  • 하지만 Iv관련해서는 따로 얘기가 없어서 Iv가 필요없는 AES/ECB/PKCS5Padding방식으로 생각하고 작업했다..
  • 테스트를 할 수 있는 암호화 값을 받고 테스트하면서 방식이 잘 못되었다는 것을 알게 되었고 AES/CBC/PKCS5Padding 방식을 사용하여 Iv값 또한 맞춰서 사용하기로 얘기했다.
  • 협업을 하게 될 경우에는 항상 궁금한 점이나 모호한 부분이 있을 때 정확히 물어보고 확실하게 정하고 넘어가야 한다는 사실을 다시한번 되새길 수 있었다.


예시 암호화 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Component
public class SecretUtil {

    private final IvParameterSpec ivSpec;
    private final Cipher cipher;
    private SecretKeySpec keySpec;

    @Value("${API-SKEY}")
    private String sKey;

    @PostConstruct
    private void init() {
        byte[] key = Base64.getDecoder().decode(sKey);
        keySpec = new SecretKeySpec(key, "AES");
    }

    public SecretUtil() throws Exception {

        byte[] iv = "정해진 방식의 Iv 사용";

        ivSpec = new IvParameterSpec(iv);
        cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
    }

    // 복호화
    public String decryptText(String encryptedText) throws Exception {
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
        byte[] decryptedTextBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
        return new String(decryptedTextBytes, StandardCharsets.UTF_8);
    }

    // 암호화
    public String encryptText(String decryptedText) throws Exception {
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        byte[] encryptedTextBytes = cipher.doFinal(decryptedText.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedTextBytes);
    }
}

또 다른 요청사항

  • 해당 암호화를 끝내고 배포하려 했으나,, 또 다른 요청사항이 들어왔다.
  • oneTimeCodeaccessToken뒤에 구분자를 이용해서 나누어 추가하고 암호화해서 보내겠다는 것.
  • 이유는 보안이 약한 것 같아 2중, 3중으로 장치를 걸겠다는 취지였다.
  • oneTimeCode는 한번 호출되면 만료가 되어 다시 쓸 수 없는 값으로 header에 추가하는 식으로 진행되었다.
  • 간단하게 split을 이용해서 구분자에 맞춰 값을 자르고 header에 넣어주고 개발 서버 배포까지 완료했다.


끝난 줄 알았지만 쿼리 스트링 관련 이슈 발생

  • 쿼리 스트링으로 암호화된 값을 보내게 되면 url encoding관련 이슈가 발생했다.


예시

1
2
3
'+' --> 공백
'/' --> '%2F'
'=' --> '%3D'

와 같이 해당 특수문자들이 변환되어 url에 표현된다.

  • 해당 이슈는 프론트에서 처리해도 되지만 보안상 우리 쪽 backEnd에서 처리하기로 했다.
    1. 디코딩 값 / 인코딩 값둘 중 어떤 값이 들어 오더라도 해결할 수 있도록 처리
    2. 유효성 검사 실패시 해당 구조에 맞춰 null값 반환


urlDecoded예시 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public String urlDecodedToken(String token){

        try {
            // URL 디코딩 기호들 처리
            String urlDecodedToken = URLDecoder.decode(token, StandardCharsets.UTF_8.name());
            urlDecodedToken = urlDecodedToken.replace(" ", "+"); // 공백을 '+'로 복원
            urlDecodedToken = urlDecodedToken.replace("%2F", "/"); // '%2F'를 '/'로 복원
            urlDecodedToken = urlDecodedToken.replace("%3D", "="); // '%3D'를 '='로 복원

            // Base64 문자열 유효성 검사
            if (!Pattern.compile("^[a-zA-Z0-9+/=]+$").matcher(urlDecodedToken).matches()) {
                return null;
            }

            // Base64 디코딩
            Base64.getDecoder().decode(urlDecodedToken);

            // 디코딩 성공 시 할당
            return urlDecodedToken;

        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException(e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }


느낀 점

  • 이번에 처음 비대면으로 협업을 진행하면서 생각보다 재밌으면서 소통이 힘들다는 점을 느꼈다.
  • 또한, 같은 회사에 소속된 팀원이 아닌 협업사에 소속된 팀원과 작업을 해야 했기에 어떤 식으로 소통해야 하는지 몰랐던 부분도 크다.
  • 그리고 확실하지 않은 부분은 확실할 때 까지 지속적으로 소통을 해야 하고, 상대방 또한 모든 사항을 알수 없기 때문에 내가 아는 정보를 최대한 많이 전달하고 모르는 부분은 전달 받아야 한다.
  • 보안 관련해서 크게 신경을 쓰면서 작업한 적이 없었어서 이번 작업이 보여주기 식 이라고 생각했다.
  • 너무 의미없이 때려 박는 것이 아닌가 하는 생각도 들었다.
  • 하지만, 보안은 신경써서 나쁠 것은 없고 의미 없어 보이더라도 가능한 선에서 걸어 두는 것이 좋다고 생각했다.
  • 이번 계기로 작업을 진행할 때 다시 한번 생각해 보고 귀찮더라도 최대한 보안을 신경쓰면서 작업해야 겠다는 생각이 들었다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.