27일) 언제까지 Input type="text"쓸꺼야! 예쁜 웹에디터를 쓰자 ! /react-quill/ dynamic import/ XSS 크로스사이드스크립트 / 제가 개발자인데도 무서워요 잘 막아볼게요 OWASP TOP 10 / Code Camp FE 6기 / 코드캠프

kimareum·2022년 4월 20일
0

코드캠프6기

목록 보기
27/36
post-thumbnail

오늘 수업의 요약 !
오늘은 웹에디터에 관해 배웠습니다.
웹 에디터는 많은 종류가 있었죠! React-draft-wysiwyg와 React-quill 이 대표적이였고, 그 중 우리는 React-quill을 사용했습니다!


React-quill을 설치하고 연결시켜주는데 중요한 포인트가 있었죠!
React-quill의 css 부분도 함께 import 시켜주어야했던 점!
그리고, dompurify 또는 react-quill 등의 라이브러리를 사용하기 위해 react와 next의 구조적인 차이를 이해해야할 필요가 있었습니다.


Next의 작동 특성! Browser에 화면을 그려주기 전 Front-end 서버에서 한번 그려준다고 했었죠!
next는 서버사이드렌더링(SSR)을 지원하는 react 전용 서버사이드 프레임워크이기 때문에, 프론트엔드 서버에서 먼저 한 번 그려지고,
브라우저에서 다운로드 받은 html, css, javascript를 다시 한 번 그리게 됩니다. 이 작업을 pre-rendering이라고 했습니다!
이 두가지의 구조를 비교하는 과정을 diffing, 이 내용들을 종합적으로 브라우저에 그려내는 과정을 hydration이라고 했던것 기억하시죠!?
하지만 pre-rendering 시에 document나 window가 존재하지 않아 그려줄 수 없기때문에 웹 에디터를 그려줄 수 없었습니다!


이를 해결하기 위해 우리는 react-quill을 Browser일때만 import 되도록 직접 조작해주었죠! 이를 dynamic import 라 했습니다!
dynamic을 next/dynamic에서 가져와 사용했고, 가져온 dynamic내 에서 react-quill을 가져와 ServerSideRendering은 하지 않도록 설정해 주었습니다! (const ReactQuill = dynamic(()=>import(‘react-quill’),{ssr:false})기억나시죠?)
또한, if(typeof window === "undefined") 를 사용하여 서버에서 처리할 내용(또는 그 반대 !==를 활용하여 브라우저에만 처리할 내용)을 적어주는 방법이 있었습니다.
Hook-form과 함께 사용할때는 register로 인식시켜줄 수 없고, 자동으로 onChange인식이 적용되지 않아 setValue()를 통해 값을 넣어주고, trigger()를 통해 changeEvent를 감지하여 Hook-form에게 알려주었죠!


웹에디터의 원리는 생각보다 간단했습니다!
단지 우리가 작성한 내용의 앞 뒤로 <u <strong 등의 태그를 붙여서 데이터베이스에 저장했을 뿐이였죠!
한가지 문제가 있었습니다! 모든 내용을 지워도 <p<br<p가 남아있었죠!
그렇기 때문에 우리는 이 찌꺼기를 없애주기 위해 삼항연산자를 사용해 처리해 주었습니다!
이 내용들이 문자열이 아닌, 태그의 기능 그 자체로 인식하게 만들기 위해 dangerouslySetInnerHTML 이라는 속성을 사용해서 해결했습니다.
이 방식에는 보안상 큰 맹점이 있었습니다! 태그처럼 자바스크립트로 해킹 기능을 작성해놨다고 가정했을때, 누군가 글 상세보기를 클릭하면, 작성된 자바스크립트 코드가 실행될 것이고, 해킹( Ex.토큰 탈취, 탈취된 토큰으로 유해 게시물 생성 및 중요 게시물 삭제 등)을 당할 수 있습니다. 이런 방식을 CrossSiteScript (XSS) 라고 했습니다!
하지만 다행히, react-quill 라이브러리에서는 태그를 의미하는 단어인 '<', '>' 등을 <, >와 같이 변경하여 문제가 발생하지 않도록 사전에 차단해 주었습니다!
하지만, 직접 실습해본 토큰 탈취 예제 처럼 react-quill만 믿고 방심 했다가는, 다른 어느 곳에 취약점이 있을지 알 수 없습니다!
따라서, 이러한 코드를 방지하기 위해, 보안을 강화할 부분에 dompurify의 sanitize를 활용하여 막아줬습니다.(Browser환경에서만 실행 시켜주기위해 process.browser && 를 사용해주었죠!) 이러한 코딩을 시큐어코딩 이라고 한다 했습니다!


마지막으로 Hydration css 오류도 알아봤습니다!
위에서 설명했던 diffing 과정이 실행되고, server에서의 태그와 browser에서의 태그를 비교하고 서버에서 한번 그려준 내용을 Browser에 그려내는 과정(Hydration)에서 발생하는 오류였습니다! 이를 해결하기 위해서는 Browser의 태그 개수와 server안 태그 개수를 맞춰주어야 했죠!
간단했습니다! browser가 아니라면 빈 태그를 보여주는 조건을 걸어 개수를 맞춰 해결해 주었죠!
오늘 배운 내용은 웹서비스 전체 구조적인 시야를 넓힐 수 있는 좋은 기회였습니다.

- 오늘의 알고리즘 문제


나의 뿌듯한 풀이

function solution(s, n) {
    let bigger= ["A","B","C","D","E","F","G","H","I","J","K","L",
                 "M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
    let small=["a","b","c","d","e","f","g","h","i","j","k","l","m",
               "n","o","p","q","r","s","t","u","v","w","x","y","z"]
    let result =""
 for(i=0; i<s.length; i++){
   if(bigger.includes(s[i])){
     if(bigger.indexOf(s[i])+n >25 ){
       result += bigger[bigger.indexOf(s[i])+n-26]
     }else{
       result += bigger[bigger.indexOf(s[i])+n]
     }
   } else if (small.includes(s[i])){
     if(small.indexOf(s[i])+n>25){
       result += small[small.indexOf(s[i])+n-26]
     }else {
       result += small[small.indexOf(s[i])+n]
     }     
   }else if(s[i] === " "){
            result += " "
            }
    } return result 
    }

- 웹에디터 이해 ( 그냥 input text는 너무 안예뻐!)

  • npm의 react-draft-wysiwyg, react-quill을 찾아 들어가보자
    -> react의 유명한 폼 라이브러리이다 !
    -> wysiwyg 에디터를 만들 수 있는 사람 !
    -> toast ui editor (노션처럼 만들수 있는 에디터)

  • 우리는 react quill 써보자 임포트해서!
    -import 사이트 긁어올때 commonjs는 옛날방식이야 !

    저 onchange는 quil 자체의 함수이름이다!
    event가 함수에 들어오는것이 아니라,
    value가 바로 들어온다고 쓰여있는걸 확인할 수 있다 !
    (그러니 라이브러리 사용할때 설명서를 잘읽자)





    그래서 나는 이렇게하면 reactquil이 될 줄 알았는데 안돼!
    document~가 없다고 한다
    이유는, 역시 프리렌더링 할때 프론트엔드서버에서는 react quill을 읽을수 없어서 !
    {typeof window !== "undefined" && (어라 얘도 안되네 !

다이나믹 임포트를 해보자 !

react 전용 프레임워크가 nextjs잖아 !
그래서 프리렌더링같은게 진행이되는거야
순수한 react 쓰면 프리렌더링 없음 거기서 쓰면 이런 오류가 생기진 않겠지만,
우리는 nextjs를 쓰니까 .. 다이나믹 임포트를 해야한다 !
그래서 reactquill의 임포트 방식을 바꿔봤다!

const ReactQuill = dynamic(()=> import("react-quill"),{ ssr : false})
서버사이드렌더링에선 안읽을게요 false라고 해준거에요 !
다이나믹 임포트 방법 !

  • react hook form을 써보자 !
    (배웟는데 생소하네...함수 언제까지 다 만들지 말고 hook form 쓰는 습관!!)
    import {useForm} from 'react-hook-form'
    const {register, handleSubmit} = useForm({
    mode:"onChange"
    }).. 이거 기억나지 ! ?? !


    콘솔에 value찍히는거 보면 우리의 value값이 태그안에 감싸진채 찍히는걸 볼수 있다 !
    -onChangeContents를 react-hook-form에 연결해보자
    강제로
    -왜 아무것도 안썼는데 빈값이 아니게 나오지 ?

    -> 그래서 저기 적혀있는 값이면 ! 빈값으로 인식해주자라고 코드에 지정을 해준다

    ->mode:"onChange"입력해서 에러 실시간 체인지띄워준건데,
    react quill의 라이브러리는 강제로 onchange를 시켜줘야한다
    (밑에 적힌 onChange는 이름만 onChange야)
    -> 리액트 퀼을 입력했지만, contents라는 key를 onChange할거다를 추가해줘야한다
    useform에서 trigger가져와서 trigger("contents") 추가해주자
  • form 태그 써서 handlesubmit쓰는 부분 익히자 !
  • if~else if~else 복잡하니까 우리 배웠던거,
    ealry exit 있을때 말고 없을때로 조건지정 !


    -> React quill을 써서 등록하고 조회하는 부분 다시 복습해보자
    -콘솔에 찍어봤던 value자체가 그냥 그 data로 들어가 버리는건 어쩌지 ?
    태그를 태그로 인식하게 해주자 ! 모든걸 문자열로 인식하게 말고 !

    ->dangerouslySetInnerHTML의 등장....!
    ={{ __html:data?.fetchBoard.contents}}이렇게 적어주니까 되네 ?
    -> 태그자체로 인식하도록 해주는 방법이다 ! (presenter에 적는 부분)


    근데 뭔가 덴져러스,,,,위험해보이네 왜 ?

    - 크로스사이드스크립트(XSS)(localstorage는 위험햇)


    콘솔에 보면 react quill이 onchange에 막아놓긴했다
    그래서 이렇게 텍스트로 나오도록 해줬어 요거까지는

    얘는 근데 reactquill에서만 해당하고 백엔드에 직접 들어가서 뮤테이션을 보내보면..!(자주사용되는 해킹기법을 사용하여 뚫어봐보자 (이미지태그사용)..원두멘토님은 모르는게 뭘까?)

    이렇게 등록을 하고 !

    마우스 사러 들어왔는데 토큰을 탈취당하는 현장....

-> 아무튼 setinnerhtml이 굉장히 위험하다!

- npm에 dompurify라는게 있어 !
-> 얘네를 사용하면 태그가 있는 문장이 자동으로 차단이 된다 !
yarn add dompurify 해주자, 타입스크립트도 !


-데이터를 조회하는 페이지에서 임포트해주자
dangerouslySetInnerHTML={{
__html:Dompurify.sanitize(data?.fetchBoard.contents)
}

아까 적었던 부분을 이렇게 코드를 작성해주니 보안완료!
인줄 알았는데 우리 next쓰니까 프리렌더링할때 프론트 서버에서 dom못읽으니까 태그를 브라우저 일때만 화면에 보이게끔 해주자
-> Cross-Site-Script(XSS)
사이트를 가로질러서 스크립트를 삽입하는 공격을 막아주는법!

- OWASP TOP 10

위에서 알아본 크로스 사이드 스크립트 처럼 웹에는 여러 공격들이 있고,
그 만큼 사람들은 보안에 신경을 많이 쓰게 되었다!
OWASP란 Open Web Application Security Project의 약자로
오픈소스 웹 애플리케이션 보안 프로젝트 라는 뜻.
주로, 웹 관련 정보노출이나 악성파일 및 스크립트,
보안 취약점을 연구하며 10대 취약점을 발표한다!
매년 탑 1을 유지하는 것이 Injection
SQL쿼리문을 작성할때 조건을 통해 데이터를 주고 받는데, 이 조건을 직접 조작하여 공격합니다. 이를 현재는 ORM을 사용해 막는중!
그 외 여러 공격들이 있는데, 이는 OWASP 발표에서 확인 가능하다!


https://ko.wikipedia.org/wiki/OWASP


이러한 컴터공학 정보들은 개발자로서 알아둘것 !
개발자는 이렇게 매일 보안 부분에서 민감하게
뚫리지 않는 방패와 뚫지 못하는게 없는 창의 싸움을 해야한다....!

  • SQL인젝션은 아직도 일어나고 있다 (qqq || 1===1 똑똑해 아주~)

- hydration-issue(하이드레이션에서 css문제발생!)

  • 나는 분명 이렇게 썼는데
 <div style={{color: "indianred"}}>작성자: {data?.fetchBoard.writer} </div>
        <div style={{color: "pink"}}>제목: {data?.fetchBoard.title}</div>
        {/* <div>내용:{data?.fetchBoard.contents} </div> */}
        {typeof window !== "undefined" && (
            <div  style={{color: "red"}} dangerouslySetInnerHTML={{
                __html:Dompurify.sanitize(data?.fetchBoard.contents)
            }}>
        </div>
            )}
            <div style ={{color: "green"}}>상품가격 : </div>

왜 색깔이 이렇게 나오지 ?

서버에서 프리렌더링할때 자바스크립트, 함수바인딩..이런거 안하고
태그의 위치들만 확인하고,
서버에서 최종적으로 그릴때 (하이드레이션) 기능들을 추가한다그랬지!


1. 아까 window아니면 그려주지 않은 dompurify빼고 렌더했었지
2. 그래서 그 div는 인식하지 못하고, 브라우저와서
3. 갑자기 조건 생겨서 dompurify div를 그리려 하는거지
4. 그래서 내가 정한 색깔을 읽지못하거 나오는거!
이러한 문제들을
hydration 이슈라고 한다
해결은 간단하다 서버에서 그릴때도 div개수를 맞춰주면 된다

  • 아주 유익한 동기의 코드리뷰 시간
    -> 사실 코드리뷰 시간은 매주 틈틈이 있어서 혼자 정리를 했었다! 다만 동기들의 자료가 블로그에 내 맘대로 올리는것 같아 쓰지 않으려 했지만, 코드리뷰자체가 모두가 공유하고 싶은 자료를 발표하는것이기 때문에 오늘 좋은 부분이 있어 올리려 한다 !!
    코드리뷰 시간이 별것 아닌것 같았지만, 시간이 지날수록 다른 사람들은 코드짤때 어떻게 생각하고, 에러를 확인할때 어떻게 확인하는지 등등을 알수 있어서 아주 유익하고 소중한 시간이다 !

    -자주뜨는 에러에 관한 부분

    -400에러(보통은 graphql 오류)
    -> gql확인 쿼리확인하자 ! 잘못된 문법을 나타낸다
    -나머지 에러에 관한 부분

    -이번주배운내용 정리부분
    컴포넌트 prev, recoil, closure, HOC,react-form, yup 잘 정리해주셨다
    내가 헷갈리던 부분 클로져랑 hoc ...

오늘 알고리즘 reduce쓴 풀이

const lower = "abcdefghijklmnopqrstuvwxyz"
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
function solution(s,n){
    const answer =s.split("").reduce((acc,cur)=>{
        const word = lower.includes(cur) ? lower : upper;
        //더한 인덱스값을 변수로 정해줌 
        let idx = word.indexOf(cur) + n;
        // word[idx] === undefined 요밑에 if절안에..!
        if(idx >=26){
            idx -= 26
        }
        return acc + (
        cur === " " ? " ": word[idx])
    },"")
    return answer;
}

아스키코드 쓴 풀이(생소하지만 잘 생각하면 이해가 쉬움..!)

// 대문자 65(A) - 90(Z)
// 소문자 97(a) - 122(z)
function solution(s,n){
  let answer = ""
  for(let i =0; i<s.length; i++){
    if(s[i]===" "){
      answer +=" ";
      continue;
    }
    let idx =s[i].charCodeAt() +n;
    if(idx>122|| idx>90 &&(idx-n)<97){
      //소문자 z(122)를 넘어가거나
      //대문자 Z(90)을 넘어가면서 소문자를 넘어가지 않을때 
      idx -=26;    
  }
    answer += String.fromCharCode(idx)
  }	return answer
}```
profile
SUNNY SUMMER ! 같이 일하고 싶은 개발자 여름이의 주절주절 TIL 블로그. 기술 블로그 : https://summereum.notion.site/SUNNY-SUMMER-5d8c27f89cf040088999a027e9ddbfdd

0개의 댓글