타 사용자의 개인정보 탈취 가능 여부 확인 회고
해당 이슈가 발생한 이유
- 기존에 협업사 측에서 회원의 정보를 보내줄 때 고정된 5~6자리 대소문자의
UID
를 보냄 UID
를 타 사용자의 것으로 변조할 경우회원 정보 탈취 위험
첫 번째 해결 방법
UID
의 길이가 짧고 바로 회원 정보를 탈취할 수 있어서accessToken
방식으로 받도록 수정accessToken
은header
에서Bearer
로 받도록 한다.
간단하게 해결할 생각
1
2
3
4
5
// 기존에 UID를 받아서 처리하던 방식
@Valid @RequestBody LoginDTO data
// header에서 token을 받아서 처리하도록 수정
@RequestHeader("Authorization") String authorizationHeader
header
로token
을 받고
예시 코드
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
중복 체크를 하고headers
에set
해 주는 방식으로 처리 하려고 했다.
프론트 이슈 대응
- 예를 들어 앱을 업데이트 하지 않고 사용하는 사용자가 있다면 해당 방식으로 기존에 사용하던
api
를 사용할 수 없게 된다. - 즉, 기존에 사용하는 기능을 유지하면서
token
방식을 추가 하거나api
를 나눠서 작업해야 했다.
나의 선택
- 처음에는 하나의
api
에서 분기 처리하려 했으나,UID
방식은 결국에는 사라져야 하는 방식이기 때문에 유지보수를 생각해서api
를 따로 나눠서 작업하고, 후에UID
방식을 삭제할 수 있도록 작업 하려고 생각했다. - 하지만 프론트 쪽에서는
GET
방식의 쿼리 스트링으로만 값을 가져올 수 있었다. - 그로 인해
header
에서token
을 받아 오는 것이 아닌LoginDTO
에서UID
와token
값을 받을 수 있게 수정하기로 했다.
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);
}
}
또 다른 요청사항
- 해당 암호화를 끝내고 배포하려 했으나,, 또 다른 요청사항이 들어왔다.
oneTimeCode
를accessToken
뒤에 구분자를 이용해서 나누어 추가하고 암호화해서 보내겠다는 것.- 이유는 보안이 약한 것 같아 2중, 3중으로 장치를 걸겠다는 취지였다.
oneTimeCode
는 한번 호출되면 만료가 되어 다시 쓸 수 없는 값으로header
에 추가하는 식으로 진행되었다.- 간단하게
split
을 이용해서 구분자에 맞춰 값을 자르고header
에 넣어주고 개발 서버 배포까지 완료했다.
끝난 줄 알았지만 쿼리 스트링 관련 이슈 발생
- 쿼리 스트링으로 암호화된 값을 보내게 되면
url encoding
관련 이슈가 발생했다.
예시
1
2
3
'+' --> 공백
'/' --> '%2F'
'=' --> '%3D'
와 같이 해당 특수문자들이 변환되어 url
에 표현된다.
- 해당 이슈는 프론트에서 처리해도 되지만 보안상 우리 쪽
backEnd
에서 처리하기로 했다.디코딩 값 / 인코딩 값
둘 중 어떤 값이 들어 오더라도 해결할 수 있도록 처리- 유효성 검사 실패시 해당 구조에 맞춰
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 라이센스를 따릅니다.