본문 바로가기

Node.js

20210331 Nodejs05 모듈 활용, 입 출력 정보 보안(XSS), html entities, sanitize-html, pm2(kill, --no-daemon, --watch, --ingnore), 생활코딩

생활코딩

  • 생활코딩 : Node.js 수업
  • 생활코딩님의 강의를 들으면서 공부한 내용을 정리하는 용도로 작성되었으며, 본내용이 틀릴 수 도 있습니다.

Node.js

Node.js App - 모듈의 활용

  • lib 폴더 : library를 뜻하고 재사용 가능한 작은 조각의 로직, 프로그램을 말함
  • module.exports 를 바로 해당 object에 줘도 되고, 아니면 나중에 module.exports = template 형식으로 줘도 된다.
// lib/template.js
  // 바로 module.exports 주는 경우
module.exports = {
    html : (title, list, body, control) => {
      return `
      <!doctype html>
      <html>
      <head>
      <title>WEB - ${title}</title>
      <meta charset="utf-8">
      </head>
      <body>
      <h1><a href="/">WEB</a></h1>
      ${list}
      ${control}
      ${body}
      </body>
      </html>
      `;
    },

    list : (files) => {
      var list = '<ul>';
      files.forEach((file) => {
         list = list + `<li><a href="/?id=${file}">${file}</a></li>`
      })
      list = list + '</ul>';
      return list;
    }
  }

  // 나중에 변수명으로 주는 경우
const template = {
    html : (title, list, body, control) => {
      return `
      <!doctype html>
      <html>
      <head>
      <title>WEB - ${title}</title>
      <meta charset="utf-8">
      </head>
      <body>
      <h1><a href="/">WEB</a></h1>
      ${list}
      ${control}
      ${body}
      </body>
      </html>
      `;
    },

    list : (files) => {
      var list = '<ul>';
      files.forEach((file) => {
         list = list + `<li><a href="/?id=${file}">${file}</a></li>`
      })
      list = list + '</ul>';
      return list;
    }
  }

module.exports = template;

  • 모듈 사용
var template = require('./lib/template.js');
  • pm2에서는 watch속성을 주어서 ignore한것을 제외하고는 바뀌는 것을 감지해서 바로 반영되게 할수 있기에 template module이 변경되어도 저장시에 바로 반영이 된다.



App - 입력 정보에 대한 보안 (Security) @진짜 중요!

  • 보안 위험 요소
    • 나중에 data 폴더에 database 소프트웨어를 사용하게 되는 경우 database는 id, password 가 있어야만 접근하여 데이터를 가져올 수 있음
    • 만약, password.js 파일에 database 소프트웨어에 대한 id, password를 보관한다면
    • queryString을 통한 get 방식의 render과정에서 file에 접근하여 읽어 가져오는 것을 활용해서 만약에 queryString으로 id=../password.js라고 한다면 그에 대한 파일을 읽어서 웹에 표현되기 때문에 보안장치의 열쇠가 누출 된다.
    • 그래서 위와 같이 queryString의 request를 통해서 모든 파일에 접근하여 들여다 볼수 있는 문제점이 생긴다.

  • 사용자로 부터 경로가 들어노는 모든 것을 바꿔줄 필요가 있음
    • 들어오는 정보, 나가는 정보 모두 오염될수 있음 그래서 모든것을 철저히 의심 해야함

  • 들어온 queryString에서의 id 부분은 file를 찾기 위한 title이자 path를 의미하게 됨 그래서 id 부분을 통해서 다른 경로의 파일까지 볼수 있기에 path.parse를 통해서 들어온 queryString id 즉, path를 들어오면 모두 쪼개서 필요한 부분만을 걸러 사용할수 있게 함 그러면 아무리 어떤 경로를 넣어도 이미 필터링 되어서 readFile이 이루어지기 때문에 path에 dir 관련해서 값을 넣을 수 없게 된다. parse된 path에서 base만 사용되기 때문
var path = require('path');

path.parse('./password.js')

// {
//   root: '',
//   dir: '.',
//   base: 'password.js',
//   ext: '.js',
//   name: 'password'
// }

  • 위의 path.parse를 사용해서 현재 app에 적용해보기
    • 직접적으로 path를 사용하는 경우를 모두 고쳐주어야 함
    • Read part에서는 다른 파일을 볼수 있어서
    • delete part에서는 다른 파일을 제거할 수 있어서
    • create part에서는 다른 위치에 위험한 파일을 만들수 있어서
    • update part에서는 다른 위치의 파일을 수정할 수 있어서
var http = require('http');
var fs = require('fs');
var url = require('url');
var qs = require('querystring');
var template = require('./lib/template.js');
var path = require('path');



var app = http.createServer(function(request,response){
    var _url = request.url; // request.url은 요청한 pathname + queryString = path
    var queryData = url.parse(_url, true).query; // queryString
    var pathname = url.parse(_url, true).pathname;
    if (pathname === `/`) {
      // Read Home (just '/')
      if(!queryData.id) {
        fs.readdir('./data', (err , files) => {
          var list = template.list(files);
          var title = 'Welcome';
          var description = 'Hello, Node.js';
          var html = template.html(title, list, `<h2>${title}</h2>
          <p>${description}<p>`, `<a href="/create">create</a>`);
          response.writeHead(200);
          response.end(html);

        });
      // Read a specific page ('/?id=specific item in list')
      } else {
        fs.readdir('./data', (err , files) => {
          var filteredId = path.parse(queryData.id).base;
          if (files.includes(filteredId)){
            fs.readFile(`data/${filteredId}`, 'utf8', (err, description) => { 
              var list = template.list(files);
              var title = queryData.id;
              var html = template.html(title, list, `<h2>${title}</h2>
                <p>${description}<p>`,
                `<a href="/create">create</a>
                <form action="/delete_process" method="post"> 
                  <input type="hidden" name="id" value="${title}">
                  <input type="submit" value="delete"> 
                </form>
                <a href="/update?id=${title}">update</a>`);              
              response.writeHead(200);
              response.end(html); 
            });
          } else {
            response.writeHead(404);
            response.end('Not found');
          };
        });
      };
    // Create a page
    } else if(pathname === '/create'){
      fs.readdir('./data', (err , files) => {
        var list = template.list(files);
        var title = 'WEB - create';
        var html = template.html(title, list, `
        <form action="/create_process" method="post">
          <p><input type="text" name="title" placeholder="title"></p>
          <p>
            <textarea name="description" placeholder="description"></textarea>
          </p>
          <p>
            <input type="submit">
          </p>
          </form>
        `, '');
        response.writeHead(200);
        response.end(html);

      });
    // Create a page and Make a file in data folder
    } else if(pathname === '/create_process') {
      // if (request.method == 'POST') {
        var body = '';

        request.on('data', (data) => {
            body += data;
        });

        request.on('end', () => {
          var post = qs.parse(body);
          var title = post.title;
          var description = post.description;
          var filteredTitle = path.parse(title).base;
          fs.readdir('./data', (err , files) => { // check a same file's name
            if (files.includes(filteredTitle) === false){
              fs.writeFile(`data/${filteredTitle}`, description, 'utf8', (err) => {
                if(err){
                  console.log(`\n-- '${filteredTitle} file' : Failed Create --\n`)
                  throw err;
                } else{
                  console.log(`\n-- '${filteredTitle} file' : Completed Create --\n`)
                  response.writeHead(302, {Location: `/?id=${filteredTitle}`});
                  response.end();
                }
              });
            } else {
              response.writeHead(404);
              response.end('failed Create : Require a different name.\nTry to do again!');
            };
          });
        });
    // }
    // Update a specific page
    } else if(pathname === '/update') {
      fs.readdir('./data', (err , files) => {
        var filteredId = path.parse(queryData.id).base;
        if (files.includes(filteredId)){
          fs.readFile(`data/${filteredId}`, 'utf8', (err, description) => { 
            var list = template.list(files);
            var title = filteredId;
            var html = template.html(title, list, 
              `
              <form action="/update_process" method="post">
              <input type="hidden" name="id" value="${title}">
              <p><input type="text" name="title" placeholder="title" value="${title}"></p>
              <p>
                <textarea name="description" placeholder="description">${description}</textarea>
              </p>
              <p>
                <input type="submit">
              </p>
              </form>
              `,
              `<a href="/create">create</a> <a href="/update?id=${title}">update</a>`);              
            response.writeHead(200);
            response.end(html); 
          });
        } else {
          response.writeHead(404);
          response.end('Not found');
        };
      });
   // Update a specific page and Update specific data file in data 
    } else if (pathname === '/update_process') {
      var body = '';

      request.on('data', (data) => {
          body += data;
      });

      request.on('end', () => {
        var post = qs.parse(body);
        var id = post.id;
        var title = post.title;
        var filteredId = path.parse(id).base;
        var filteredTitle = path.parse(title).base;
        var description = post.description;
        // !!!!!!!!!!!!!!!!!!! important!!!!!!!!!!!!!!!
        fs.readdir('./data', (err , files) => {
          // Check same file's name and Prevent from Updating another file that we already have had
          // first compare old title with new title
          if (filteredTitle !== filteredId) {
            // seconde compare new title with other files
            if (files.includes(filteredTitle) === false){
              fs.rename(`data/${filteredId}`, `data/${filteredTitle}`, (err)=>{
                  if (err) {
                    console.log(`\n-- '${filteredId} -> ${filteredTitle} file' : Failed Rename --\n`);
                    throw err;
                  } else {
                    console.log(`\n-- '${filteredId} -> ${filteredTitle} file' : Completed Rename ! --\n`);
                    fs.writeFile(`data/${filteredTitle}`, description, 'utf8', (err) => {
                      if(err){
                        console.log(`\n-- '${filteredId} -> ${filteredTitle} file' : Failed Update description --\n`);
                        throw err;
                      } else{
                        console.log(`\n-- '${filteredId} -> ${filteredTitle} file' : Completed Update ! --\n`)
                        response.writeHead(302, {Location: `/?id=${filteredTitle}`});
                        response.end();
                      }
                    });
                  }
              });
            } else {
              response.writeHead(404);
              response.end('failed Update : Require a different name.\nTry to do again!');
            }; 

          } else {
            fs.writeFile(`data/${filteredTitle}`, description, 'utf8', (err) => {
              if(err){
                console.log(`\n-- '${filteredId} -> ${filteredTitle} file' : Failed Update description --\n`);
                throw err;
              } else{
                console.log(`\n-- '${filteredId} -> ${filteredTitle} file' : Completed Update ! --\n`)
                response.writeHead(302, {Location: `/?id=${filteredTitle}`});
                response.end();
              }
            });

          }; 
        });
      });
    // Delete a specific file in data folder
    } else if (pathname === "/delete_process") {
      var body = '';

      request.on('data', (data) => {
          body += data;
      });

      request.on('end', () => {
          var post = qs.parse(body);
          var id = post.id;
          var filteredId = path.parse(id).base;
          fs.unlink(`data/${filteredId}`, (err)=>{
              if (err) {
                console.log(`\n-- '${filteredId} file' : Failed Delete --\n`)
                throw err;
              } else {
                console.log(`\n-- '${filteredId} file' : Completed Delete --\n`)
                response.writeHead(302, {Location: `/`});
                response.end();
              }
          });
      });
    } else {
    response.writeHead(404);
    response.end('Not found');
  }

});
app.listen(3000);

  • 추가적으로 개선한 부분
    • 근데 이미 Read Part의 경우 아래와 같이 readdir를 통해서 data dir에 들어온 queryData.id(path)와 맞는 이름이 있는지 검증해서 출력 되었기 때문에 Not found로 막아 졌다.
    • 하지만, 그렇게 하면 if 문이 증가해서 코드가 더 커질수 있긴 해서 path.parse를 사용하는게 좋을것 같긴 함
    • 그래도 update, create의 중복 이름에 대한 덮어씌우기 현상을 고치려면 위와 같은 file name check 가 필요함
// Read part : check a same file and path problem
fs.readdir('./data', (err , files) => {
  if (files.includes(queryData.id)){ 
      // do something
  } else {
    response.writeHead(404);
    response.end('Not found');
  }
}

// Create, Update part : check a same file
fs.readdir('./data', (err , files) => {
  if (!files.includes(queryData.id)){ 
      // do something
  } else {
    response.writeHead(404);
    response.end('failed Create : Require a different name.\nTry to do again!');
  }
}

  • 더 필요한 부분 : 입력 값 유효성 검증, module 또는 객체화, 함수형 프로그래밍을 하여 코드의 중복성을 줄일 필요가 있음, 특히 예외 처리 부분



App - 출력 정보에 대한 보안 (Security) @진짜 중요!

  • 파일 수정 및 생성시 description에 script 태그로 js 명령을 넣어버리면 만들어진 list의 link를 누르면 해당 script 코드가 실행 되어 제대로된 기능을 수행 못함

  • XSS(Cross Site Scripting)

    • XSS : 게시판이나 웹 메일 등에 자바 스크립트와 같은 스크립트 코드를 삽입 해 개발자가 고려하지 않은 기능이 작동하게 하는 치명적일 수 있는 공격이다. 또한 대부분의 웹 해킹 공격 기법과는 다르게 클라이언트 즉, 사용자를 대상으로 한 공격이다.
  • xss 공격이란 ? (by ARGOS)


  • Reflected XSS : 공격자가 스크립트를 포함한 URL을 사용자에게 노출시켜 해당 스크립트를 포함한 response를 받아옴
<!--Reflected XSS :  누르는 순간 merong 알림이 뜸 -->
<script>
alert('merong!')
</script>
  • Stored XSS : 웹사이트의 게시판에 스크립트 삽입하는 공격 방식 (게시글 클릭 유도)
<!-- 누르면 튕겨져서 해당 url로 가버림 -->
<script>
location.href = "https://naver.com";
</script>

  • 발생 가능한 위험 :
    • 쿠키 정보 및 세션 ID 획득하여 정상 사용자인척 할 수 있음
    • 시스템 관리자 궈한 획득 (악성데이터를 넣어 사용자가 실행시키게 할수 있음)
    • 악성코드 다운로드 (자체로 악성 프로그램을 다운로드 할수는 없지만 악성 스크립트가 있는 url을 클릭유도해 그런 프로그램을 다운받게 유도 가능)
    • 거짓 페이지 노출 (원래 페이지와 관련 없는 페이지 표시하여 개인정보 유출 위험)

  • 방지법 :
  • Script를 없애는 것 (script 문자 필터링(서버측에서))
  • script의 꺽쇠를 그대로 표현되게 하기 (HTML entities)
// script
<script>
location.href = "https://naver.com";
</script>

// html entities 사용 -> web 페이지에서 그대로 출력 가능
&lt;script&gt;
location.href = "https://naver.com";
&lt;/script&gt; 



script 태그 비활성화 (sanitize : 살균하다, 불쾌한 부분을 제거하다.)


Sanitize-html 설치

  • npm을 통한 패키지 관리 (가져다 쓴 패키지 관리해줌)
    • npm init
    • 기본적으로 자신의 폴더이름이 패키지 이름이 되어 자신의 app을 패키지로 관리
    • package.json 파일 생성됨
      • 프로젝트에 대한 정보가 생성됨
  • npm install -S sanitize-html : -S 옵션은 -g와 다르게 해당 프로젝트에서 부품으로만 사용되게 깔아짐 (-g 는 어디에서든지 전역에서 쓸수 있게 해줌)
  • node_mudule라는 폴더가 생성되어 안에 깔려지게 되고 우리 프로젝트의 package.json에 dependencies(의존성)에 표시 됨 (어떤 외부 프로그램에 의존하는지 알려줌)
"dependencies": {
    "sanitize-html": "^2.3.3"
  }
  • node_module에 sanitize말고 다른 것들이 있는 이유는 sanitize가 의존하는 프로그램을 같이 설치 하기 때문인데 이를 npm이 관리해줌



Sanitize-html 사용

  • 사용시 script태그는 안에 있는 내용 및 태그 모두 없애 버리고, h2, h1 등의 별로 위협적이지 않은 태그는 내용만 살려줌 (즉, 태그는 모두 없애고 script 태그(예민한 태그)는 내용까지 없앰)
  • data file에는 모두 저장 되지만 브라우저에 출력시 문제가 되는 것이므로 출력시 사용하여 제거해주는 것임
  • sanitizeHtml( string or variable , option)
    • option : object를 넣으면 됨 (properties : allowedTags, allowedAttributes, allowedIframHostname ....

  • 사용 예시
const clean = sanitizeHtml(dirty, {
  allowedTags: [ 'b', 'i', 'em', 'strong', 'a' ],
  allowedAttributes: {
    'a': [ 'href' ]
  },
  allowedIframeHostnames: ['www.youtube.com']
});

  • default 설정 값
allowedTags: [
  "address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4",
  "h5", "h6", "hgroup", "main", "nav", "section", "blockquote", "dd", "div",
  "dl", "dt", "figcaption", "figure", "hr", "li", "main", "ol", "p", "pre",
  "ul", "a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn",
  "em", "i", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp",
  "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "caption",
  "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr"
],
disallowedTagsMode: 'discard',
allowedAttributes: {
  a: [ 'href', 'name', 'target' ],
  // We don't currently allow img itself by default, but this
  // would make sense if we did. You could add srcset here,
  // and if you do the URL is checked for safety
  img: [ 'src' ]
},
// Lots of these won't come up by default because we don't allow them
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
// URL schemes we permit
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'tel' ],
allowedSchemesByTag: {},
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
allowProtocolRelative: true,
enforceHtmlBoundary: false

var sanitizeHtml = require('sanitize-html'); // sanitize-html


// Read a specific page ('/?id=specific item in list')
      } else {
        fs.readdir('./data', (err , files) => {
          var filteredId = path.parse(queryData.id).base;
          if (files.includes(filteredId)){
            fs.readFile(`data/${filteredId}`, 'utf8', (err, description) => { 
              var list = template.list(files);
              var title = queryData.id;
              var sanitizedTitle = sanitizeHtml(title); // sanitizer!
              var sanitizedDescription = sanitizeHtml(description, {
                allowedTags: ['h1']
              }); // sanitizer!
              var html = template.html(sanitizedTitle, list, 
                `<h2>${sanitizedTitle}</h2><p>${sanitizedDescription}<p>`,
                `<a href="/create">create</a>
                <form action="/delete_process" method="post"> 
                  <input type="hidden" name="id" value="${sanitizedTitle}">
                  <input type="submit" value="delete"> 
                </form>
                <a href="/update?id=${sanitizedTitle}">update</a>`);              
              response.writeHead(200);
              response.end(html); 
            });
          } else {
            response.writeHead(404);
            response.end('Not found');
          };
        });
      };

<h1>Yes YES!</h1>
<script>this is dirty</script>

// Yes YES! h1 태그까지 적용 되어서 출력
// script는 없음



API (Application Programming Interface)

  • interface : 개발자들의 약속된 조작 장치
  • Application을 프로그래밍 하기 위해 만들어진 interface를 API라고 함
  • 궁금한 것이 있음 API를 살펴 보아야 함.

pm2 보충

  • pm2 kill : 실행한 모든 프로세스 중지 및 삭제
  • 옵션
    • --no-daemon : pm2 실행시 log 도 같이 출력 되게 함
    • --watch : 수정사항을 읽어서 server restart
    • --ignore-watch="data/* sessions/*" : data, sessions 디렉토리 수정은 restart 무시