본문 바로가기
Study/js

axios에서 요청 URL에 [ 와 ] 가 포함되면 자바 서블릿 기반 톰캣 웹 서버에서 에러가 나는 이유와 해결책

by 유경호 2023. 3. 31.
반응형

문제

톰캣 7.x 버전부터 RFC 3986, RFC7230 규정을 준수하여 특수문자를 URL에 포함하여 보내는 것을 block 하고 있음. 혹여나 [와 ]가 URL에 담겨 오면 톰캣 웹서버 단에서 Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986 에러 메시지를 발생시키며 400 Bad Request를 내려줌.

 

Chrome과 axios에서는 기본적으로 URL에 percent-encoding(이하 ‘%인코딩’)을 하긴 하지만 특수하게 [와 ]만은 %인코딩을 하지 않고 서버에 전달함. 이 때문에 서버에 [와 ]가 URL에 담겨 전달되는 현상과 RFC 7230, RFC 3986 규정 미준수 에러가 발생함.

 

하여 axios에서 보내는 파리미터를 자바스크립트 내장객체인 encodeURI() 혹은 encodeURIComponent()로 %인코딩하여 보내봤음. 하지만 axios에서 해당 문자열을 한 번 더 %인코딩하여 ‘%’가 ‘%25’로 %인코딩되는 불참사가 일어남.

// <script>alert(\"xss\")</script>[]를 encodeURI()한 문자열
%3Cscript%3Ealert(%5C%22xss%5C%22)%3C%2Fscript%3E%5B%5D

// 위 문자열을 한 번 더 %인코딩한 문자열
%253Cscript%253Ealert(%255C%2522xss%255C%2522)%253C%2Fscript%253E%255B%255D

 

하여 웹 서버(톰켓, 자바 스프링부트)에서 %인코딩에 대한 디코딩을 한 번 해주지만 %3Cscript%3Ealert(%5C%22xss%5C%22)%3C%2Fscript%3E%5B%5D 문자열이 최종적으로 컨트롤러 메서드로 바인딩 됨. 그래서 컨트롤러 메서드에서 해당 파라미터를 또 한 번 수동으로 URI decoding을 해줘야 하는 불필요해 보이는 추가 프로세스가 발생함.

이를 단순히 %인코딩을 수동으로 디코딩 해서 쓰는 것으로 해결하고 넘어갈 수도 있겠으나 너무나 찝찝했기에 정확히 원인을 분석하고 더 좋은 해결책을 강구해 보기로 함.

원인

결론적으로는 axios에서 해주는 %인코딩 때문에 이런 일이 일어나는 건데, 문서에도 나타나있지 않아 직접 코드를 들여다봤음. 이를 살펴보겠음.

axios에 존재하는 request methods({get, post, put, delete, patch 등...})들은 모두 config 객체를 받게 되어 있음. 이 곳에  paramsSerializer 속성을 주입할 수 있음. 해당 속성은 encode와 serialize가 두 속성으로 구성되어 있음.  둘 다 파라미터에 대한 직렬화 및 인코딩 방식을 커스터마이징 할 수 있도록 제공하는 속성인데, 아래 형태의 함수를 넘겨주면 됨.

 

parameterSerializer?:ParamsSerializerOptions 구조 예시

const confing = {
	parameterSerializer: {
		encode?: ParamEncoder,
		serialize?: CustomParamsSerializer;
	}
}
  • ParamEncoder 정보
    interface ParamEncoder {}
    
    호출 형태
    (value: any, defaultEncoder: (value: any) => any): any; 
  • CustomParamsSerializer 정보
    interface CustomParamsSerializer {}
    
    호출 형태
    (params: Record<string, any>, options?: ParamsSerializerOptions): string;

위 속성이 주어지지 않으면 axios 내부에 내장된 기본 인코딩 방식으로 파라미터를 %인코딩 하게 되는데. 이게 `[`랑 `]`를 서버로 전달하게 만듬. 이 부분을 살펴보겠음.

 

axios의 get() api를 호출할 때, 아래의 두 arguments를 전달하게 됨.

  • url: string
  • config?: AxiosRequestConfig<D>

get() api를 호출되면 axios가 최종적으로 요청할 URL을 만들기 위해 buildURL.js의 buildURL() 함수를 실행함.

import utils from '../utils.js';
import AxiosURLSearchParams from '../helpers/AxiosURLSearchParams.js';

function encode(val) {
  return encodeURIComponent(val).
    replace(/%3A/gi, ':').
    replace(/%24/g, '$').
    replace(/%2C/gi, ',').
    replace(/%20/g, '+').
    replace(/%5B/gi, '[').
    replace(/%5D/gi, ']');
}

export default function buildURL(url, params, options) {
  if (!params) {
    return url;
  }
  // parameterSerializer.encode 옵션 검사 후 없으면 기본 함수 encode() 사용
  const _encode = options && options.encode || encode;
  // parameterSerializer.serialize 옵션 검사, 없으면 undefined 주입
  const serializeFn = options && options.serialize;

  let serializedParams;

  if (serializeFn) {
    serializedParams = serializeFn(params, options);
  } else {
    serializedParams = utils.isURLSearchParams(params) ?
      params.toString() :
      new AxiosURLSearchParams(params, options).toString(_encode);
  }

  if (serializedParams) {
    const hashmarkIndex = url.indexOf("#");

    if (hashmarkIndex !== -1) {
      url = url.slice(0, hashmarkIndex);
    }
    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
  }

  return url;
}
  • parameterSerializer.encode 나 parameterSerializer.serialize 가 주어지지 않으면 기본 axios 내장 기본 encode() 함수를 사용하여 query string에 포함될 파라미터들을 %인코딩함.

만약 사용자가 요청한 params 의 타입이 URLSearchParams이 아니라 plain Ojbect라면 AxiosURLSearchParams Object를 생성하여 AxiosURLSearchParams.toString() 을 통해 URL을 생성하게 되는데, 구조는 아래와 같음.

import toFormData from './toFormData.js';

function encode(str) {
  const charMap = {
    '!': '%21',
    "'": '%27',
    '(': '%28',
    ')': '%29',
    '~': '%7E',
    '%20': '+',
    '%00': '\\\\x00'
  };
  return encodeURIComponent(str).replace(/[!'()~]|%20|%00/g, function replacer(match) {
    return charMap[match];
  });
}

function AxiosURLSearchParams(params, options) {
  this._pairs = [];

  params && toFormData(params, this, options);
}

const prototype = AxiosURLSearchParams.prototype;

prototype.append = function append(name, value) {
  this._pairs.push([name, value]);
};

prototype.toString = function toString(encoder) {
  const _encode = encoder ? function(value) {
    return encoder.call(this, value, encode);
  } : encode;

  return this._pairs.map(function each(pair) {
    return _encode(pair[0]) + '=' + _encode(pair[1]);
  }, '').join('&');
};
export default AxiosURLSearchParams;

 

buildURL.encode()를 보면 알겠지만 encode() 함수은 사용자가 넘긴 파라미터를 한 번 encodeURIComponent() 하고 :, $, ,, +, [, ]를 %인코딩한 것에서 다시 온전한 문자로 원복 시킴.

 function encode(val) {
  return encodeURIComponent(val).
    replace(/%3A/gi, ':').
    replace(/%24/g, '$').
    replace(/%2C/gi, ',').
    replace(/%20/g, '+').
    replace(/%5B/gi, '[').
    replace(/%5D/gi, ']');
}

이로 인해 최종적으로 axios에서 전송하는 URL은 :, $, ,, +, [, ]가 다시 원복된 형태의 URL 문자열이 됨.

 

그리고 Chrome에서 [와 ]를 제외한 나머지 특수문자들은 다시 인코딩을 해버리기 때문에 결과적으로 자바 서블릿 기반 톰캣 서버에 [와 ]만이 온전하게 전달되는 것임. 이것이 이번 이슈의 핵심 원인임.

 

참고로 chrome에서는 %인코딩이 안 된 문자들을 기본적으로 자동으로 인코딩 해주고, %인코딩이 된 문자들은 인코딩하지 않는 것으로 보임. 이는 fetch() 함수를 통해 확인한 바 있음.

 

웹서버인 톰캣에서 RFC 3986, RFC7230 규정에 의거해 서버에서 400에러를 반환했던 이유가 이것임.

해결

axios.get(url[, config])

axios.get() api에는 두 가지 arguments가 존재함.

  • url: string
  • config?: AxiosRequestConfig<D>

requet config에는 많은 요소들이 들어갈 수 있으나, 그중에 params를 보겠음.

// `params` are the URL parameters to be sent with the request
// Must be a plain object or a URLSearchParams object
// NOTE: params that are null or undefined are not rendered in the URL.
params: {
  ID: 12345
}
  • params에는 plain object와 URLSearchParams object, 두 가지 타입의 객체가 들어감.

params에 plain object 주입 시 위 문제 파트에서 언급했던 바와 같이 axios에서 정한 기본 로직대로 %인코딩을 해주는데, [와 ]는 %인코딩 해주지 않음.
하지만 URLSearchParams 타입의 object를 주입하게 되면 URLSearchParams.toString() 을 사용하여 요청 URL을 생성하기 때문에 [와 ]가 %인코딩되어 전송되는 것을 확인할 수 있음.

그래서 URL에 들어가는 파라미터는 아래와 같이 URLSearchParams 객체를 생성하여 params를 전달하게 되면 문제를 해결할 수 있음.

const searchText = '<script>alert(\"xss\")</script>[]';
// plain object로 생성하는 대신 URLSearchParams 객체 생성
const params = new URLSearchParams();
params.append('searchText', searchText);

// URLSearchParams 객체를 config.params에 전달
$axios.get('some-url', { params }).then((res) => {/* do something */})
// URLSearchParams.toString()으로 'searchText=%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E%5B%5D' 전달

이리하여 서버에 전송되는 URL에는 [와 ]가 깨끗하게 %인코딩 되어 전달 되는 것을 확인할 수 있음.

 

axios가 제공하는 기본적인 인코딩 방식 외에 params에 대한 Custom한 인코딩이 필요할 때는 parameterSerializer.encode 나 parameterSerializer.serialize를 주입하여 요청하면 되겠음.

 

현재 해당 이슈를 해결은 하였으나 아래 두 가지 의문이 남아있음.

  • axios가 url에 인코딩된 :, $, ,, +, [, ] 문자열을 왜 다시 원복 하는지
  • Chrome에서는 왜 [ 와 ]를 %인코딩 하지 않고 요청을 하게 되는지

두 의문점은 향후 확인하기로 함.

 

여기까지 요청된 URL에 [ 와 ]가 포함되면 서버가 받지 않는 이유와 해결책임.

참조

반응형

'Study > js' 카테고리의 다른 글

Javascript 정리  (0) 2021.05.08