기타

[RESTful API] 민감한 파라미터 통한 데이터 조회시 GET vs POST

quantumee 2025. 1. 13. 16:19

보안

RESTful한 개발을 위해 공부할때 처음으로 배운게 요청 method에 따른 API 역할이었다.get은 조회, post는 서버 작업으로 알고 있었지만 실제 프로젝트에 들어가면 이를 지키기가 쉽지 않다.

 

그 대표적인 예시 상황이 파라미터로 넣어줄 값이 외부에 노출되면 안될 민감한 데이터인 경우다.예를 들어 사용자의 개인정보, api-key 등을 get 요청으로 쿼리스트링으로 보내줄 경우 url에 노출되고 길이 제한으로 암호화 역시 어렵다. 또한 GET 요청은 default로 캐싱 대상이 될 수 있어 브라우저 캐시에 남아 유출 위험이 있다.

 

https를 사용할 경우 MITM(Man-in-the-Middle)같은 공격은 방지 할 수 있어 어느정도 보안성은 보장되지만 그래도 여전히 url이 브라우저 주소창에 남아있으며 북마크 등을 사용할 경우 민감성 데이터가 그대로 공유될 우려가 있다.

 

RESTful API 설계 원칙을 지키면서 보안을 잃지 않는 방법이 뭘까 고민해봤다.

 

 


1. Header 사용

: GET 요청으로 파라미터 값이 url에 노출이 되는 게 문제라면 노출되지 않게 쿼리 스트링이 아니라 header에 담아 보내면 될거라고 생각했다. 

 

실제로 header에 담아 보내고 https를 사용할 시에는 url에 노출도 없고 중간에 정보를 탈취당할 우려도 없어서 어느정도 해결되었다고 생각했다.

 

- 예시 코드

아래는 js axios로 header에 민감성 데이터를 담아 보내는 예시 코드이다.

const response = await axios.get('https://your-springboot-server/api/user-info', {
      headers: {
        'Authorization': 'Bearer your-access-token', // 민감 데이터
        'X-User-Id': '12345' // 사용자 ID와 같은 민감 정보
      },
    });

 

- Header key 이름 정의

: header에 들어가는 key의 명칭은 확정된 명칭은 아니지만 표준화된 이름은 존재한다.

     1) Authorizaion : 인증과 관련한 값을 담는 키이며 RFC 표준에 해당한 공식적인 HTTP 헤더이다.

                               ㄴ> 공식적인 명칭이 있는 만큼 사용하는게 혼란을 방지할 수 있겠다.

     2) X-User-Id : 표준이 아닌 사용자 지정 정의 헤더이다.

                            간혹 API를 사용하다보면 X- 로 시작하는 헤더를 발견할 수 있는데 이 표식은 사용자가 정의한 비공식적                              헤더임을 밝히는 접두사(IETF 표준)였으나 2012년 이후 사용하지 않을 것을 권장한다.

                           ㄴ> 대신, 고유하고 명확한 이름을 사용해야한다 ( 관습적으로 최근에도 사용중이다.) 

 


2. 캐싱 방지 

: GET 요청을 할때 캐싱으로 민감한 데이터가 저장된다면 캐싱을 못하게 header 설정을 하면 된다고 생각했다.

http 요청에서 캐싱 방지를 하는 header 설정은 아래와 같다

 

- Cache-Control (**핵심설정)

     1) no-cache : 클라이언트의 요청마다 서버에서 최신 데이터를 받아오도록 함

     2) no-store : 응답 데이터가 캐시에 저장되지 않도록 지정함

     3) must-revalidate : 클라이언트가 캐시된 데이터를 사용할 때 반드시 서버와 재검증이 필요하도록 지정함

                                    (ㄴ> 서버에서 확인되지않으면 캐시된 데이터를 사용할 수 없음)

- Pragma

     1) no-cache : HTTP/1.0에서 사용된 레거시 시스템용 설정 ( HTTP/1.1이상 부터는 Cache-Control이 우선 적용됨)

- Expires

     1) 0 : 만료기한 없이 즉시 만료

     2) <date> : 현재시간을 만료시간으로 지정 ( 0 과 동일한 효과 )

     => Cache-Control의 위 설정이 적용되지 않았다면 유효함

 

 

- 예시 코드

아래는 js의 axios로 요청을 보냈을 때 예시 코드이다.

const response = await axios.get('https://your-springboot-server/api/user-info', {
      headers: {
        'Cache-Control': 'no-cache, no-store, must-revalidate', // 캐싱 방지
        'Pragma': 'no-cache',
        'Expires': '0',
      },
    });

 

 


3. POST 사용

 

- 여전히 남아있는 GET 사용시 문제점

위 방법을 통해 GET 으로 개발을 해도 여전히 문제가 많이 남아있다.

GET의 경우 브라우저 및 프록시 로그에 기록되기 때문에 데이터가 추적될 가능성이 여전히 남아있고

URL의 길이 제한이 브라우저 상에서는 보통 2000자가 있고 apache나 Nginx도 자체적으로 제한을 두고 있어서 전송할 데이터가 잘릴 위험이 있다.

 

- RESTful API 설계 이유 재확인

RESTful API 설계는 규칙보다 "목적과 상황에 맞는 설계" 가 중요하므로 POST를 사용해도 무방하다란 결론에 다다랐다.

일반적인 데이터 조회는 GET으로 하데  민감성 데이터를 담을 경우는 POST로 사용해서 보안요소를 좀더 쉽게 처리하도록 하는게 오히려 RESTful 하다고 생각하게 되었다.

 

하지만 이럴 경우 그동안에 end-point naming 규칙을 수정해야했다.

 

- end-point naming 규칙 재설립

기존 user-info와 관련된 API는  

 

   | GET  example.com/user-info      => 특정 유저의 정보 조회

   | POST  example.com/user-info    => 특정 유저의 정보 저장

 

이렇게 되어있었는데 

정보 조회도 POST로 바꿔야 한다면 end-point 명명도 다시해야했다.

 

어떻게 naming을 해야 적절할까 고민하다 정한 방법은

   | POST  example.com/user-info/query      => 특정 유저의 정보 조회

   | POST  example.com/user-info                => 특정 유저의 정보 저장

이렇게 고친 거였다.

 

뒤에 붙힌 단어는 프로젝트 진행시 팀원들과 공유해서 일관성을 유지한다면 이걸로도 충분히 RESTful 한 설계라 생각한다.

 


결론

민감성 데이터일 경우,

복잡하게 보안요소를 챙길 방법을 추가하기 보다는 POST를 사용하고

그 설계를 일관성있게 확정한다면 충분히 RESTful 하다.