🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Phase 2 공격 기법 이해와 방어 실전 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 27. · 2 Views

Phase 2 공격 기법 이해와 방어 실전 가이드

웹 애플리케이션 보안의 핵심인 공격 기법과 방어 전략을 실습 중심으로 배웁니다. 인증 우회부터 SQL Injection, XSS, CSRF까지 실제 공격 시나리오를 이해하고 방어 코드를 직접 작성해봅니다.


목차

  1. 인증_우회_공격과_방어_전략
  2. 브루트포스_공격_시뮬레이션
  3. XSS_취약점_실습
  4. SQL_Injection_공격과_방어
  5. CSRF_방어_구현
  6. 네트워크_공격_시나리오_분석
  7. 악성코드_동작_원리

1. 인증 우회 공격과 방어 전략

어느 날 김개발 씨가 회사의 로그인 시스템 코드를 리뷰하다가 이상한 점을 발견했습니다. "왜 이렇게 간단하게 인증을 처리하지?" 그때 보안팀에서 긴급 공지가 내려왔습니다.

"누군가 관리자 계정으로 로그인했습니다. 하지만 관리자는 오늘 휴가입니다."

인증 우회 공격은 정상적인 로그인 절차를 거치지 않고 시스템에 접근하는 공격 기법입니다. 마치 놀이공원에서 줄을 서지 않고 뒷문으로 몰래 들어가는 것과 같습니다.

공격자는 취약한 인증 로직, 세션 관리 결함, 또는 JWT 토큰의 허점을 이용합니다. 이를 이해하면 더 견고한 인증 시스템을 설계할 수 있습니다.

다음 코드를 살펴봅시다.

// 취약한 인증 코드 - 절대 이렇게 작성하면 안 됩니다
function vulnerableLogin(username, password) {
  // 위험: 클라이언트에서 전달받은 role을 그대로 신뢰
  const user = db.findUser(username);
  if (user.password === password) {
    return { token: createToken({ role: req.body.role }) };
  }
}

// 안전한 인증 코드 - 서버에서 역할을 결정합니다
function secureLogin(username, password) {
  const user = db.findUser(username);
  // 비밀번호는 반드시 해시 비교
  const isValid = await bcrypt.compare(password, user.hashedPassword);
  if (isValid) {
    // 역할은 DB에서 가져온 값만 사용
    return { token: createToken({ role: user.role, exp: Date.now() + 3600000 }) };
  }
  throw new AuthenticationError('Invalid credentials');
}

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 보안팀의 긴급 공지를 받고 급하게 로그인 시스템 코드를 열어보았습니다.

코드를 살펴보던 중 소름이 돋았습니다. 로그인할 때 사용자가 보낸 역할 정보를 그대로 토큰에 넣고 있었던 것입니다.

선배 개발자 박시니어 씨가 다가와 상황을 파악했습니다. "이건 전형적인 인증 우회 취약점이에요.

공격자가 요청에 role: admin을 넣으면 관리자가 되어버리는 거죠." 그렇다면 인증 우회 공격이란 정확히 무엇일까요? 쉽게 비유하자면, 인증 우회는 마치 신분증 검사대에서 가짜 신분증을 보여주거나, 아예 검사대를 우회하는 뒷문을 찾는 것과 같습니다.

정상적인 절차를 따르지 않고도 허가된 영역에 들어갈 수 있다면, 그것이 바로 인증 우회입니다. 인증 시스템이 취약하면 어떤 일이 벌어질까요?

가장 흔한 시나리오는 권한 상승입니다. 일반 사용자가 관리자 권한을 획득하면 모든 데이터에 접근할 수 있습니다.

개인정보 유출, 서비스 마비, 금전적 피해까지 이어질 수 있습니다. 실제로 많은 보안 사고가 이런 인증 우회에서 시작됩니다.

공격자들이 사용하는 대표적인 인증 우회 기법을 알아봅시다. 첫 번째는 파라미터 조작입니다.

위 코드처럼 클라이언트가 보낸 역할 정보를 그대로 신뢰하면, 공격자는 간단히 요청을 조작해 관리자가 될 수 있습니다. 두 번째는 JWT 알고리즘 변경입니다.

일부 라이브러리에서 알고리즘을 none으로 설정하면 서명 검증을 건너뛸 수 있습니다. 안전한 인증 시스템을 구축하려면 어떻게 해야 할까요?

가장 중요한 원칙은 서버에서 모든 것을 결정한다는 것입니다. 클라이언트가 보낸 역할, 권한, 만료 시간 등을 절대 신뢰하면 안 됩니다.

모든 민감한 정보는 서버의 데이터베이스에서 가져와야 합니다. 위의 안전한 코드를 한 줄씩 살펴보겠습니다.

먼저 bcrypt.compare를 사용해 비밀번호를 안전하게 비교합니다. 평문 비교는 절대 금물입니다.

그다음 역할 정보는 요청에서 가져오는 것이 아니라 user.role처럼 데이터베이스에서 조회한 값을 사용합니다. 마지막으로 토큰 만료 시간도 서버에서 설정합니다.

실제 현업에서는 어떻게 적용할까요? 대부분의 기업에서는 **다단계 인증(MFA)**을 도입하고 있습니다.

비밀번호만으로는 부족하기 때문입니다. 또한 로그인 시도 횟수를 제한하고, 의심스러운 접근은 추가 인증을 요구합니다.

OAuth나 OIDC 같은 표준 프로토콜을 사용하는 것도 좋은 방법입니다. 주의할 점도 있습니다.

자체 인증 시스템을 만들 때 가장 흔한 실수는 세션 고정 공격을 방어하지 않는 것입니다. 로그인 성공 후에는 반드시 새로운 세션 ID를 발급해야 합니다.

또한 로그아웃 시 서버 측에서도 세션을 무효화해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨와 함께 코드를 수정한 후, 보안팀에 보고했습니다. "앞으로는 클라이언트 입력을 절대 신뢰하지 않겠습니다." 김개발 씨는 그날 중요한 교훈을 얻었습니다.

실전 팁

💡 - 클라이언트에서 전달받은 역할, 권한 정보를 절대 신뢰하지 마세요

  • 비밀번호는 반드시 bcrypt, Argon2 같은 안전한 해시 함수로 비교하세요
  • 로그인 성공 후 세션 ID를 재생성하여 세션 고정 공격을 방어하세요

2. 브루트포스 공격 시뮬레이션

김개발 씨가 아침에 출근해서 서버 로그를 확인하던 중 이상한 패턴을 발견했습니다. 새벽 3시부터 5시까지 특정 계정에 로그인 시도가 10만 번이나 있었습니다.

"누가 이렇게 집요하게 로그인을 시도한 거지?" 비밀번호가 단순했던 그 계정은 결국 뚫리고 말았습니다.

브루트포스 공격은 가능한 모든 비밀번호 조합을 하나씩 대입해보는 무차별 대입 공격입니다. 마치 자물쇠의 모든 번호 조합을 하나씩 시도하는 것과 같습니다.

단순해 보이지만 컴퓨터의 빠른 연산 속도 덕분에 놀라울 정도로 효과적입니다. 이 공격을 이해하면 왜 복잡한 비밀번호 정책과 로그인 제한이 필요한지 알게 됩니다.

다음 코드를 살펴봅시다.

// 브루트포스 방어가 적용된 로그인 시스템
const loginAttempts = new Map();
const BLOCK_DURATION = 15 * 60 * 1000; // 15분
const MAX_ATTEMPTS = 5;

async function secureLoginWithRateLimit(username, password, ip) {
  const key = `${username}:${ip}`;
  const attempts = loginAttempts.get(key) || { count: 0, blockedUntil: 0 };

  // 차단 상태 확인
  if (Date.now() < attempts.blockedUntil) {
    const remaining = Math.ceil((attempts.blockedUntil - Date.now()) / 1000);
    throw new Error(`Too many attempts. Try again in ${remaining} seconds`);
  }

  const user = await db.findUser(username);
  const isValid = user && await bcrypt.compare(password, user.hashedPassword);

  if (!isValid) {
    attempts.count++;
    if (attempts.count >= MAX_ATTEMPTS) {
      attempts.blockedUntil = Date.now() + BLOCK_DURATION;
      await notifySecurityTeam(username, ip); // 보안팀 알림
    }
    loginAttempts.set(key, attempts);
    throw new Error('Invalid credentials');
  }

  loginAttempts.delete(key); // 성공 시 초기화
  return generateToken(user);
}

김개발 씨는 서버 로그를 보며 머리가 복잡해졌습니다. 10만 번의 로그인 시도라니, 사람이 직접 한 것은 분명 아닙니다.

박시니어 씨가 로그를 함께 분석했습니다. "이건 브루트포스 공격이에요.

자동화된 도구로 비밀번호를 무차별 대입한 겁니다." 브루트포스 공격이란 정확히 무엇일까요? 자전거 자물쇠를 생각해 보세요.

4자리 숫자 조합이라면 0000부터 9999까지 만 가지 경우의 수가 있습니다. 사람이 일일이 시도하면 며칠이 걸리겠지만, 컴퓨터는 1초에 수천 번 이상 시도할 수 있습니다.

이것이 브루트포스의 핵심 원리입니다. 공격자들은 어떤 도구를 사용할까요?

Hydra, Burp Suite, John the Ripper 같은 도구들이 널리 사용됩니다. 이 도구들은 사전 파일을 활용해 흔한 비밀번호부터 시도하는 사전 공격도 수행합니다.

password123, qwerty, 123456 같은 비밀번호는 몇 초 만에 뚫립니다. 브루트포스 공격이 성공하면 어떤 피해가 발생할까요?

계정이 탈취되면 개인정보 유출은 물론, 해당 계정으로 추가 공격이 가능해집니다. 관리자 계정이 뚫리면 전체 시스템이 위험해집니다.

또한 대량의 로그인 시도로 인해 서버 리소스가 소모되어 정상 사용자도 로그인하기 어려워질 수 있습니다. 위의 방어 코드를 자세히 살펴보겠습니다.

핵심은 로그인 시도 횟수 제한입니다. 사용자별, IP별로 로그인 시도 횟수를 추적합니다.

5회 실패하면 15분간 해당 조합에서의 로그인을 차단합니다. 이렇게 하면 공격자는 15분에 5번밖에 시도할 수 없어, 브루트포스가 사실상 불가능해집니다.

하지만 이것만으로는 부족합니다. 공격자가 IP를 바꿔가며 공격하면 IP 기반 차단을 우회할 수 있습니다.

따라서 CAPTCHA를 도입하거나, 일정 횟수 이상 실패 시 이메일 인증을 요구하는 것이 좋습니다. 또한 bcrypt의 work factor를 높여 해시 연산 자체를 느리게 만드는 것도 효과적입니다.

실제 기업에서는 어떻게 방어하고 있을까요? 대부분의 서비스에서 3-5회 로그인 실패 시 계정 잠금이나 추가 인증을 요구합니다.

Google이나 Facebook은 의심스러운 로그인 시도가 감지되면 알림을 보내고, 기기 인증을 요청합니다. 또한 reCAPTCHA를 통해 자동화된 공격을 차단합니다.

로그 모니터링도 중요합니다. 새벽 시간대에 특정 계정으로 대량의 로그인 시도가 있다면, 이는 분명한 공격 징후입니다.

SIEM(Security Information and Event Management) 시스템을 구축하면 이런 이상 징후를 실시간으로 탐지할 수 있습니다. 김개발 씨는 방어 코드를 적용한 후, 보안팀과 함께 모니터링 체계도 구축했습니다.

일주일 후 비슷한 공격이 다시 들어왔지만, 이번에는 5번 시도 후 자동으로 차단되었습니다. 로그를 보며 김개발 씨는 뿌듯한 미소를 지었습니다.

실전 팁

💡 - 로그인 시도 횟수를 IP와 계정 조합으로 제한하세요

  • 실패 횟수 초과 시 일시적 계정 잠금과 함께 보안팀에 알림을 보내세요
  • CAPTCHA나 MFA를 도입하여 자동화 공격을 차단하세요

3. XSS 취약점 실습

김개발 씨가 만든 게시판 서비스에 이상한 게시물이 올라왔습니다. 제목은 평범해 보였지만, 그 글을 클릭한 사용자들이 갑자기 로그아웃되는 현상이 발생했습니다.

알고 보니 게시물 내용에 숨겨진 스크립트가 사용자들의 쿠키를 훔쳐 외부 서버로 전송하고 있었습니다.

**XSS(Cross-Site Scripting)**는 웹 페이지에 악성 스크립트를 삽입하여 다른 사용자의 브라우저에서 실행시키는 공격입니다. 마치 누군가 도서관의 책에 가짜 쪽지를 끼워넣어 읽는 사람을 속이는 것과 같습니다.

공격자의 스크립트가 피해자의 브라우저에서 실행되므로, 쿠키 탈취, 키로깅, 피싱 등 다양한 악용이 가능합니다.

다음 코드를 살펴봅시다.

// 취약한 코드 - 사용자 입력을 그대로 렌더링
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h1>검색 결과: ${query}</h1>`); // 위험!
});

// 안전한 코드 - HTML 엔티티 이스케이프 적용
const escapeHtml = (str) => {
  const escapeMap = {
    '&': '&amp;', '<': '&lt;', '>': '&gt;',
    '"': '&quot;', "'": '&#39;'
  };
  return str.replace(/[&<>"']/g, char => escapeMap[char]);
};

app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q); // 안전하게 이스케이프
  res.send(`<h1>검색 결과: ${query}</h1>`);
});

// Content-Security-Policy 헤더 설정
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
  next();
});

김개발 씨는 게시판 코드를 열어보고 경악했습니다. 사용자가 입력한 내용을 아무런 처리 없이 그대로 HTML에 넣고 있었습니다.

공격자는 이 점을 악용해 스크립트 태그가 포함된 게시물을 작성한 것입니다. 박시니어 씨가 설명해 주었습니다.

"이건 Stored XSS라고 해요. 악성 스크립트가 서버에 저장되어 다른 사용자들이 볼 때마다 실행되는 거죠." XSS 공격에는 세 가지 유형이 있습니다.

첫 번째는 Stored XSS입니다. 게시판이나 댓글처럼 서버에 저장되는 곳에 스크립트를 삽입합니다.

가장 위험한 유형입니다. 두 번째는 Reflected XSS입니다.

검색어처럼 URL 파라미터를 통해 스크립트를 전달하고, 응답에 그대로 반영됩니다. 세 번째는 DOM-based XSS입니다.

서버를 거치지 않고 클라이언트 측 JavaScript에서 발생합니다. XSS가 성공하면 어떤 일이 벌어질까요?

가장 흔한 공격은 쿠키 탈취입니다. 공격자의 스크립트가 document.cookie를 읽어 외부 서버로 전송합니다.

세션 쿠키가 탈취되면 공격자는 피해자의 계정을 그대로 사용할 수 있습니다. 또한 키로깅으로 비밀번호를 훔치거나, 페이지 내용을 변조하여 피싱 공격도 가능합니다.

방어의 첫 번째 원칙은 이스케이프입니다. HTML에서 특별한 의미를 가지는 문자들(<, >, &, ", ')을 안전한 엔티티로 변환합니다.

<script>&lt;script&gt;로 바뀌면 브라우저는 이를 텍스트로 인식하여 실행하지 않습니다. 하지만 이스케이프만으로는 부족할 수 있습니다.

CSP(Content-Security-Policy) 헤더를 설정하면 한 단계 더 안전해집니다. CSP는 어떤 출처의 스크립트를 실행할 수 있는지 브라우저에 알려줍니다.

script-src 'self'로 설정하면 외부 스크립트는 실행되지 않습니다. 인라인 스크립트도 차단할 수 있습니다.

프레임워크를 사용하면 더 안전합니다. React, Vue, Angular 같은 현대적인 프레임워크는 기본적으로 XSS를 방지합니다.

사용자 입력을 자동으로 이스케이프하기 때문입니다. 하지만 dangerouslySetInnerHTML(React)이나 v-html(Vue) 같은 기능을 사용할 때는 여전히 주의가 필요합니다.

쿠키 설정도 중요한 방어선입니다. 세션 쿠키에 HttpOnly 플래그를 설정하면 JavaScript에서 쿠키에 접근할 수 없습니다.

XSS가 발생해도 쿠키 탈취는 막을 수 있습니다. Secure 플래그와 SameSite 속성도 함께 설정하면 더욱 안전합니다.

김개발 씨는 모든 사용자 입력에 이스케이프를 적용하고, CSP 헤더를 설정했습니다. 또한 쿠키에 HttpOnly 플래그를 추가했습니다.

"이제 XSS가 발생해도 피해를 최소화할 수 있겠네요." 박시니어 씨가 고개를 끄덕였습니다.

실전 팁

💡 - 사용자 입력은 반드시 이스케이프하고, 프레임워크의 기본 보안 기능을 활용하세요

  • CSP 헤더를 설정하여 인라인 스크립트와 외부 스크립트 실행을 제한하세요
  • 세션 쿠키에는 HttpOnly, Secure, SameSite 플래그를 설정하세요

4. SQL Injection 공격과 방어

김개발 씨가 관리하던 쇼핑몰 데이터베이스가 털렸습니다. 로그를 분석해보니 상품 검색 기능을 통해 공격이 들어온 것 같았습니다.

검색어에 이상한 문자열이 포함되어 있었습니다. ' OR '1'='1' -- 이게 대체 무슨 의미일까요?

SQL Injection은 사용자 입력을 통해 악의적인 SQL 쿼리를 주입하는 공격입니다. 마치 은행 창구에서 "제 계좌에서 100만원을 출금해주세요"라고 말하면서 몰래 "그리고 옆 사람 계좌도 조회해주세요"를 끼워넣는 것과 같습니다.

이 공격이 성공하면 데이터베이스의 모든 정보가 유출될 수 있습니다.

다음 코드를 살펴봅시다.

// 취약한 코드 - 문자열 연결로 쿼리 생성 (절대 금지!)
app.get('/products', (req, res) => {
  const name = req.query.name;
  const query = `SELECT * FROM products WHERE name = '${name}'`;
  db.query(query); // 위험! SQL Injection 가능
});

// 안전한 코드 - Prepared Statement 사용
app.get('/products', async (req, res) => {
  const name = req.query.name;
  // 파라미터화된 쿼리 - 입력값이 데이터로만 처리됨
  const query = 'SELECT * FROM products WHERE name = $1';
  const result = await db.query(query, [name]);
  res.json(result.rows);
});

// ORM 사용 (더 안전한 방법)
app.get('/products', async (req, res) => {
  const name = req.query.name;
  // ORM은 자동으로 이스케이프 처리
  const products = await Product.findAll({
    where: { name: name }
  });
  res.json(products);
});

김개발 씨는 공격 로그를 분석하며 머리를 싸맸습니다. 검색어란에 ' OR '1'='1' --를 입력하면 어떻게 될까요?

원래 쿼리가 SELECT * FROM products WHERE name = '사과'였다면, 공격 쿼리는 이렇게 바뀝니다. SELECT * FROM products WHERE name = '' OR '1'='1' --' 박시니어 씨가 화이트보드에 쿼리를 적으며 설명했습니다.

"보세요, '1'='1'은 항상 참이에요. 그래서 모든 상품이 조회됩니다.

--는 SQL 주석이라서 뒤의 내용은 무시되고요." SQL Injection은 왜 이렇게 위험할까요? 단순히 데이터 조회뿐만 아니라 UNION 구문으로 다른 테이블을 조회할 수 있습니다.

사용자 테이블에서 비밀번호 해시를 가져올 수도 있습니다. 더 심각한 경우, DROP TABLE로 테이블을 삭제하거나 INSERT로 관리자 계정을 만들 수도 있습니다.

방어의 핵심은 Prepared Statement(매개변수화된 쿼리)입니다. Prepared Statement를 사용하면 사용자 입력이 SQL 코드가 아닌 데이터로만 처리됩니다.

아무리 이상한 문자열을 입력해도 그저 검색어일 뿐, SQL 구문으로 해석되지 않습니다. 모든 현대적인 데이터베이스 라이브러리에서 이 기능을 지원합니다.

ORM(Object-Relational Mapping)을 사용하면 더 안전합니다. Sequelize, Prisma, TypeORM 같은 ORM은 내부적으로 Prepared Statement를 사용합니다.

개발자가 직접 SQL을 작성할 일이 줄어들므로 실수할 가능성도 줄어듭니다. 하지만 raw query 기능을 사용할 때는 여전히 주의가 필요합니다.

추가적인 방어 레이어도 구축해야 합니다. 데이터베이스 계정 권한을 최소화하세요.

웹 애플리케이션이 사용하는 DB 계정에 DROP, DELETE 권한이 필요한 경우는 드뭅니다. 읽기 전용 계정을 별도로 사용하는 것도 좋은 방법입니다.

또한 WAF(Web Application Firewall)를 도입하면 알려진 SQL Injection 패턴을 차단할 수 있습니다. 입력 검증도 중요한 방어선입니다.

숫자만 받아야 하는 필드에는 숫자만 허용하세요. 정규표현식으로 입력 형식을 검증하면 많은 공격을 사전에 차단할 수 있습니다.

하지만 이것만으로는 부족하며, 반드시 Prepared Statement와 함께 사용해야 합니다. 김개발 씨는 모든 SQL 쿼리를 Prepared Statement로 변경했습니다.

또한 데이터베이스 계정 권한을 최소화하고, 정기적인 보안 점검 일정도 수립했습니다. "이번 사고로 많이 배웠네요." 뼈아픈 교훈이었지만, 앞으로는 같은 실수를 반복하지 않을 것입니다.

실전 팁

💡 - 절대로 문자열 연결로 SQL 쿼리를 만들지 마세요

  • Prepared Statement나 ORM을 사용하여 입력값을 데이터로 처리하세요
  • 데이터베이스 계정 권한을 최소화하고 정기적인 보안 점검을 수행하세요

5. CSRF 방어 구현

김개발 씨가 운영하는 커뮤니티 사이트에서 이상한 일이 벌어졌습니다. 여러 사용자들이 자신도 모르게 특정 게시물에 추천을 눌렀다고 항의해온 것입니다.

알고 보니 공격자가 다른 사이트에 숨겨진 이미지 태그를 심어두었고, 그 이미지의 src가 추천 API를 호출하고 있었습니다.

**CSRF(Cross-Site Request Forgery)**는 사용자가 의도하지 않은 요청을 보내도록 속이는 공격입니다. 마치 누군가 당신의 손을 잡고 서명을 대신하게 만드는 것과 같습니다.

피해자가 이미 로그인된 상태에서 공격자의 페이지를 방문하면, 공격자가 원하는 요청이 피해자의 권한으로 실행됩니다.

다음 코드를 살펴봅시다.

// CSRF 방어 - 토큰 기반 구현
const crypto = require('crypto');

// CSRF 토큰 생성 미들웨어
function generateCsrfToken(req, res, next) {
  if (!req.session.csrfToken) {
    req.session.csrfToken = crypto.randomBytes(32).toString('hex');
  }
  res.locals.csrfToken = req.session.csrfToken;
  next();
}

// CSRF 토큰 검증 미들웨어
function verifyCsrfToken(req, res, next) {
  const token = req.body._csrf || req.headers['x-csrf-token'];
  if (!token || token !== req.session.csrfToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  next();
}

// 사용 예시
app.use(generateCsrfToken);
app.post('/api/like', verifyCsrfToken, async (req, res) => {
  await likePost(req.user.id, req.body.postId);
  res.json({ success: true });
});

// 폼에 토큰 포함
// <input type="hidden" name="_csrf" value="<%= csrfToken %>">

김개발 씨는 공격 시나리오를 재현해 보았습니다. 공격자가 만든 페이지에는 이런 코드가 숨겨져 있었습니다.

<img src="https://community.example.com/api/like?postId=123"> 이 이미지는 화면에 보이지 않지만, 브라우저는 이미지를 로드하기 위해 해당 URL로 요청을 보냅니다. 박시니어 씨가 핵심을 짚었습니다.

"문제는 브라우저가 쿠키를 자동으로 포함한다는 거예요. 사용자가 이미 로그인되어 있으면, 그 요청은 로그인된 사용자의 권한으로 실행됩니다." CSRF 공격이 성공하려면 두 가지 조건이 필요합니다.

첫째, 피해자가 공격 대상 사이트에 로그인된 상태여야 합니다. 둘째, 피해자가 공격자의 페이지를 방문해야 합니다.

공격자는 이메일, 게시판 글, 광고 등을 통해 피해자를 자신의 페이지로 유도합니다. 방어의 핵심은 CSRF 토큰입니다.

CSRF 토큰은 서버에서 생성한 랜덤 문자열입니다. 이 토큰을 폼에 숨겨진 필드로 포함시키거나, AJAX 요청의 헤더에 포함시킵니다.

서버는 요청이 올 때마다 이 토큰을 검증합니다. 공격자는 피해자의 토큰을 알 수 없으므로, 유효한 요청을 만들 수 없습니다.

위의 코드를 살펴보겠습니다. generateCsrfToken 미들웨어는 세션에 토큰이 없으면 새로 생성합니다.

verifyCsrfToken 미들웨어는 요청에 포함된 토큰과 세션의 토큰을 비교합니다. 일치하지 않으면 403 에러를 반환합니다.

이렇게 하면 공격자의 요청은 토큰이 없거나 잘못되어 실패합니다. SameSite 쿠키 속성도 강력한 방어 수단입니다.

쿠키에 SameSite=StrictSameSite=Lax를 설정하면, 다른 사이트에서 온 요청에는 쿠키가 포함되지 않습니다. 최신 브라우저들은 기본값으로 Lax를 적용합니다.

하지만 오래된 브라우저를 위해 CSRF 토큰과 함께 사용하는 것이 좋습니다. Referer 검증도 보조적인 방어 수단이 될 수 있습니다.

요청의 Referer 헤더를 확인하여 자신의 도메인에서 온 요청인지 검증합니다. 하지만 Referer 헤더는 사용자 설정이나 프록시에 의해 제거될 수 있으므로, 주 방어 수단으로 사용하기에는 부족합니다.

상태 변경 요청에는 POST를 사용하세요. GET 요청은 이미지 태그, 스크립트 태그 등으로 쉽게 유도할 수 있습니다.

반면 POST 요청을 유도하려면 JavaScript가 필요하고, 이는 다른 보안 정책에 의해 차단될 가능성이 높습니다. 데이터를 변경하는 모든 API는 POST, PUT, DELETE를 사용해야 합니다.

김개발 씨는 CSRF 토큰을 모든 폼과 AJAX 요청에 적용했습니다. 또한 세션 쿠키에 SameSite=Lax를 설정했습니다.

"이제 다른 사이트에서 악의적인 요청을 보내도 차단되겠네요."

실전 팁

💡 - 상태를 변경하는 모든 요청에 CSRF 토큰을 요구하세요

  • 세션 쿠키에 SameSite=Lax 또는 Strict를 설정하세요
  • GET 요청으로 데이터를 변경하지 마세요

6. 네트워크 공격 시나리오 분석

김개발 씨가 카페에서 작업을 하다가 이상한 경험을 했습니다. 분명 회사 내부 시스템에 접속했는데, 로그인 페이지가 평소와 조금 다르게 느껴졌습니다.

로그인을 시도했지만 실패했고, 그날 밤 보안팀에서 연락이 왔습니다. 누군가 김개발 씨의 계정으로 접속을 시도했다고 합니다.

네트워크 공격은 데이터가 오가는 네트워크 경로를 노리는 공격입니다. 마치 편지를 배달하는 우체부가 몰래 편지를 열어보고 내용을 바꿔치기하는 것과 같습니다.

공용 와이파이, ARP 스푸핑, DNS 하이재킹 등 다양한 기법이 있으며, 암호화되지 않은 통신은 쉽게 도청당할 수 있습니다.

다음 코드를 살펴봅시다.

// HTTPS 강제 적용 (Node.js/Express)
const helmet = require('helmet');

// 보안 헤더 설정
app.use(helmet());

// HTTP를 HTTPS로 리다이렉트
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') {
    return res.redirect(301, `https://${req.headers.host}${req.url}`);
  }
  next();
});

// HSTS(HTTP Strict Transport Security) 설정
app.use(helmet.hsts({
  maxAge: 31536000, // 1년
  includeSubDomains: true,
  preload: true
}));

// Certificate Pinning (모바일 앱에서 권장)
const validCertFingerprint = 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
function verifyCertificate(cert) {
  return cert.fingerprint256 === validCertFingerprint;
}

김개발 씨는 그날 무슨 일이 있었는지 되짚어 보았습니다. 카페의 와이파이에 접속했고, 평소처럼 회사 시스템에 로그인하려 했습니다.

하지만 그 와이파이는 공격자가 만든 가짜 핫스팟이었습니다. 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.

"이건 **중간자 공격(Man-in-the-Middle, MITM)**이에요. 공격자가 당신과 서버 사이에 끼어들어 모든 통신을 가로챈 거죠." 대표적인 네트워크 공격 기법을 알아봅시다.

ARP 스푸핑은 같은 네트워크에 있는 공격자가 자신을 라우터로 속이는 기법입니다. 피해자의 모든 트래픽이 공격자를 거쳐 가게 됩니다.

DNS 하이재킹은 DNS 응답을 조작하여 가짜 서버로 유도합니다. 가짜 핫스팟은 정상적인 와이파이처럼 보이지만 공격자가 운영하는 것입니다.

이런 공격이 성공하면 어떤 일이 벌어질까요? HTTP로 통신하는 모든 내용이 평문으로 노출됩니다.

비밀번호, 세션 토큰, 개인정보가 모두 탈취될 수 있습니다. 공격자는 응답을 변조하여 악성 스크립트를 삽입할 수도 있습니다.

이것이 바로 HTTPS가 필수인 이유입니다. HTTPS는 어떻게 보호할까요?

HTTPS는 TLS(Transport Layer Security)를 사용하여 통신을 암호화합니다. 공격자가 중간에서 가로채도 내용을 읽을 수 없습니다.

또한 인증서를 통해 서버의 신원을 확인합니다. 인증서가 유효하지 않으면 브라우저가 경고를 표시합니다.

하지만 HTTPS만으로는 부족합니다. 공격자는 SSL 스트리핑이라는 기법으로 HTTPS를 HTTP로 다운그레이드시킬 수 있습니다.

피해자는 HTTP로 접속하고 있다는 것을 모를 수 있습니다. 이를 방지하기 위해 HSTS(HTTP Strict Transport Security)를 사용합니다.

위의 코드를 살펴보겠습니다. helmet 라이브러리는 여러 보안 헤더를 한 번에 설정합니다.

HTTP 요청은 HTTPS로 리다이렉트됩니다. HSTS 헤더를 설정하면 브라우저는 해당 도메인에 1년간 HTTPS로만 접속합니다.

SSL 스트리핑을 시도해도 브라우저가 자동으로 HTTPS로 변환합니다. 모바일 앱에서는 Certificate Pinning을 권장합니다.

앱에 서버 인증서의 지문을 미리 저장해두고, 연결 시 검증합니다. 공격자가 가짜 인증서를 사용해도 앱이 이를 거부합니다.

다만 인증서 갱신 시 앱 업데이트가 필요하다는 단점이 있습니다. 공용 와이파이 사용 시 주의사항도 있습니다.

민감한 작업은 가급적 피하세요. 꼭 필요하다면 VPN을 사용하세요.

VPN은 모든 트래픽을 암호화된 터널로 전송하므로 도청을 방지할 수 있습니다. 또한 브라우저의 인증서 경고를 절대 무시하지 마세요.

김개발 씨는 그날의 경험을 팀과 공유했습니다. 회사 시스템에 HSTS를 적용하고, 모바일 앱에는 Certificate Pinning을 추가했습니다.

"이제 카페에서 작업해도 안심할 수 있겠죠?" 박시니어 씨가 고개를 저었습니다. "그래도 VPN은 쓰세요."

실전 팁

💡 - 모든 웹 서비스에 HTTPS를 필수로 적용하고 HSTS를 설정하세요

  • 공용 와이파이에서는 VPN을 사용하고 민감한 작업을 피하세요
  • 브라우저의 인증서 경고를 절대 무시하지 마세요

7. 악성코드 동작 원리

김개발 씨의 컴퓨터가 갑자기 느려졌습니다. CPU 사용률이 100%를 치솟고, 팬이 미친 듯이 돌아갑니다.

작업 관리자를 열어보니 알 수 없는 프로세스가 실행되고 있었습니다. 알고 보니 며칠 전 다운받은 무료 유틸리티에 암호화폐 채굴 악성코드가 숨어있었던 것입니다.

**악성코드(Malware)**는 사용자의 동의 없이 악의적인 동작을 수행하는 소프트웨어입니다. 마치 선물로 위장한 트로이 목마처럼, 유용해 보이는 프로그램 안에 숨어들어 시스템을 장악합니다.

바이러스, 웜, 트로이목마, 랜섬웨어 등 다양한 형태가 있으며, 개발자도 이러한 동작 원리를 이해해야 안전한 소프트웨어를 만들 수 있습니다.

다음 코드를 살펴봅시다.

// 악성코드 탐지를 위한 파일 해시 검증
const crypto = require('crypto');
const fs = require('fs');

// 파일 무결성 검증
function verifyFileIntegrity(filePath, expectedHash) {
  const fileBuffer = fs.readFileSync(filePath);
  const hashSum = crypto.createHash('sha256');
  hashSum.update(fileBuffer);
  const actualHash = hashSum.digest('hex');

  return actualHash === expectedHash;
}

// 의존성 취약점 검사 (package.json 분석)
async function checkDependencyVulnerabilities() {
  const { execSync } = require('child_process');
  try {
    // npm audit 실행 및 결과 파싱
    const result = execSync('npm audit --json', { encoding: 'utf-8' });
    const audit = JSON.parse(result);

    if (audit.metadata.vulnerabilities.high > 0) {
      console.warn('High severity vulnerabilities found!');
      return audit.vulnerabilities;
    }
    return null;
  } catch (error) {
    console.error('Audit failed:', error.message);
  }
}

// 서브프로세스 실행 시 안전한 방법
const { spawn } = require('child_process');
function safeExec(command, args) {
  // shell: false로 명령어 인젝션 방지
  return spawn(command, args, { shell: false });
}

김개발 씨는 컴퓨터를 포맷하고 처음부터 다시 설정해야 했습니다. 그날 이후로 무료 소프트웨어를 다운받을 때마다 신중해졌습니다.

박시니어 씨가 조언했습니다. "악성코드의 동작 원리를 알면 더 조심하게 돼요." 악성코드는 어떻게 시스템에 침투할까요?

가장 흔한 경로는 소셜 엔지니어링입니다. 유용해 보이는 프로그램, 이메일 첨부파일, 불법 복제 소프트웨어에 악성코드를 숨깁니다.

사용자가 직접 실행하도록 유도하는 것입니다. 또한 취약점 공격을 통해 사용자 동의 없이 침투하기도 합니다.

악성코드는 크게 몇 가지 유형으로 분류됩니다. 바이러스는 다른 프로그램에 자신을 복제하여 감염시킵니다.

은 네트워크를 통해 스스로 전파됩니다. 트로이목마는 정상 프로그램으로 위장합니다.

랜섬웨어는 파일을 암호화하고 금전을 요구합니다. 크립토재킹은 김개발 씨가 당한 것처럼 몰래 암호화폐를 채굴합니다.

개발자가 특히 주의해야 할 것이 있습니다. 바로 **공급망 공격(Supply Chain Attack)**입니다.

npm, PyPI 같은 패키지 저장소에 악성 패키지가 업로드되는 경우가 있습니다. 유명 패키지와 비슷한 이름(타이포스쿼팅)을 사용하거나, 인기 패키지가 탈취되어 악성 코드가 삽입되기도 합니다.

위의 코드에서 방어 방법을 살펴보겠습니다. verifyFileIntegrity 함수는 파일의 해시값을 검증합니다.

다운로드한 파일이 변조되지 않았는지 확인할 수 있습니다. npm audit을 정기적으로 실행하여 의존성의 보안 취약점을 점검해야 합니다.

서브프로세스를 실행할 때는 shell: false로 명령어 인젝션을 방지합니다. 안전한 개발 환경을 위한 실천 사항이 있습니다.

의존성을 추가할 때는 해당 패키지의 인기도, 유지보수 상태, 최근 활동을 확인하세요. lock 파일(package-lock.json, yarn.lock)을 사용하여 정확한 버전을 고정하세요.

CI/CD 파이프라인에 보안 스캔을 포함시키세요. 런타임 보안도 중요합니다.

프로덕션 환경에서는 최소 권한 원칙을 적용하세요. 애플리케이션에 불필요한 권한을 부여하지 마세요.

Docker 컨테이너를 사용한다면 root가 아닌 사용자로 실행하세요. 파일 시스템 접근, 네트워크 접근을 필요한 범위로 제한하세요.

김개발 씨는 팀의 CI/CD 파이프라인에 npm audit를 추가했습니다. 의존성을 추가할 때는 팀 리뷰를 거치기로 했습니다.

"이제 악성 패키지가 침투하기 어려워졌어요." 박시니어 씨가 덧붙였습니다. "정기적으로 의존성을 업데이트하는 것도 잊지 마세요."

실전 팁

💡 - 의존성 추가 시 패키지의 신뢰도와 최근 활동을 확인하세요

  • npm audit, Snyk 등으로 정기적인 취약점 검사를 수행하세요
  • 최소 권한 원칙을 적용하고 불필요한 시스템 접근을 제한하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Security#Authentication#XSS#SQLInjection#CSRF

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.