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 페이지에서 그대로 출력 가능
<script>
location.href = "https://naver.com";
</script>
script 태그 비활성화 (sanitize : 살균하다, 불쾌한 부분을 제거하다.)
- npm 에서 지원하는 모듈 사용해 보자
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 무시