웹 브라우저의 SOP 정책 때문에 일반적으로 다른 오리진에 요청을 보내고 그 응답 값을 받아오는 것은 불가능하다. 이를 우회하기 위한 대표적인 공격이 바로 Cross-Site Script (XSS) 공격이다. 대신 XSS 공격은 공격 대상 오리진에 반드시 공격자의 입력이 들어가 스크립트를 삽입해야하고, 피해자가 해당 페이지를 방문해야한다는 전제조건이 존재한다.
Cross-Site Search (XS-Search) 공격은 공격 대상 오리진에 스크립트를 삽입하거나 해당 페이지를 직접 방문하지 않고, 피해자가 공격자가 의도한 서버에만 접속하는 것만으로 SOP를 우회하여 공격 대상 오리진의 비밀 정보를 유출할 수 있는 강력한 공격기법이다.
**Cross-Site Search (XS-Search)**는 쿼리 기반 시스템을 이용해 이용자의 비밀 정보를 유출하는 공격 기법이다. 부채널 공격의 일종으로 Blind SQL Injection과 비슷한 형태로 공격이 이루어진다.
일반적으로 다른 오리진에 요청을 보내고 응답값을 읽어오는 것은 SOP 정책에 위반되기 때문에 불가능하다. 이 때 XS-Search 공격은 SOP 정책을 위반하지 않는 선에서 다른 오리진에 요청을 보내고 요청에 소요된 시간, 응답 코드, 창의 프레임 개수 등의 요소를 활용해 비밀 정보를 유출한다.
아래 예제는 XS-Search 공격이 발생할 수 있는 예제 코드이다. GET 파라미터로 쿼리 형태의 입력을 받고, 만약 비밀 값이 쿼리 값으로 시작한다는 조건이 부합한다면 “OK”라는 메시지와 함께 200 응답 코드를, 아니라면 “NO”라는 메시지와 함께 404 응답 코드를 반환한다.
secret = "this_is_secret_value"
@app.route('/search')
def search_secret():
query = request.args.get("query", "_")
if secret.startswith(query):
return "OK", 200
else:
return "NO", 404
secret 값이 이용자의 비밀 정보일 때, 일반적으로 공격자의 페이지서 해당 오리진으로 xhr 요청을 보내게 되면 SOP 정책에 의해 응답 값을 받아올 수 없다.
아래 코드는 일반적인 XMLHttpRequest를 이용해 요청을 보내고 응답 값과 응답 코드를 받아오려는 시도를 하는 코드이다.
var xhr = new XMLHttpRequest();
xhr.open('GET', '<http://localhost:8000/search?query=t>');
xhr.onload = function () {
console.log('Status Code:', xhr.status);
console.log('Response Text:', xhr.responseText);
};
xhr.send();
해당 코드를 이용해 요청하게 되면 CORS 에러가 발생하면서 실패하게 되며, 아래 로그에서 확인할 수 있다.

브라우저는 SOP에 구애 받지 않고 외부 출처에 대한 접근을 허용해주는 경우가 존재한다. 예를 들면, 이미지나 자바스크립트, CSS 등의 리소스를 불러오는 <img>, <style>, <script> 등의 태그는 SOP의 영향을 받지 않는다.
XS-Search는 이와 같이 SOP에 구애받지 않는 태그를 활용하여 공격한다. 아래 코드는 script 태그를 이용해 요청의 응답 코드가 200인지 404인지 구분하는 코드이다.
현재 문서에 script 태그를 생성하고 src 속성을 공격 대상 오리진의 URL로 설정한 후, 로드하는 데 성공했는지 실패했는지의 여부를 통해 응답 코드가 200인지 404인지 구분할 수 있다. 로드에 성공했다면 onload 핸들러가, 실패했다면 onerror 핸들러가 호출되기 때문에 이를 통해 구분할 수 있다.