iBetter Books
수정

보안 고려사항

스크립트가 사용자 입력을 받거나 파일을 처리하거나 외부 데이터를 다룬다면, 보안을 고려해야 합니다. 웹 애플리케이션의 SQL 인젝션과 마찬가지로, 쉘 스크립트에도 명령어 인젝션 같은 취약점이 있습니다.

입력 검증 — 사용자 입력을 명령어에 직접 넣지 않기

사용자가 입력한 값을 그대로 명령어에 사용하면 위험합니다. 악의적인 입력이 예상치 못한 명령어를 실행시킬 수 있습니다.

# 파일: input_validation.sh#!/usr/bin/env bashset -euo pipefail# 위험한 패턴: 입력값을 검증 없이 사용dangerous_list() {    local user_input="$1"    ls "$user_input"   # user_input이 "-la /etc"이면?}# 안전한 패턴: 입력값 검증 후 사용safe_list() {    local user_input="$1"    # 1. 절대 경로인지 확인    if [[ "$user_input" != /* ]]; then        echo "오류: 절대 경로만 허용합니다." >&2        return 1    fi    # 2. 허용 문자만 포함하는지 확인 (알파벳, 숫자, /,  -, _,  . 만 허용)    if [[ ! "$user_input" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then        echo "오류: 허용되지 않은 문자가 포함되어 있습니다." >&2        return 1    fi    # 3. 디렉토리 탈출 시도 방지    if [[ "$user_input" == *".."* ]]; then        echo "오류: 상위 디렉토리 참조는 허용되지 않습니다." >&2        return 1    fi    ls -- "$user_input"}echo "파일을 나열할 디렉토리를 입력하세요."read -r user_dirsafe_list "$user_dir"

--는 옵션의 끝을 알립니다. ls -- "$dir"처럼 사용하면 변수 값이 -la처럼 -로 시작해도 옵션으로 해석되지 않습니다.

명령어 인젝션 방어

명령어 인젝션은 사용자 입력이 명령어의 일부로 해석될 때 발생합니다.

# 파일: injection_demo.sh#!/usr/bin/env bash# 위험: eval을 사용하거나 변수를 따옴표 없이 사용# 입력값: "hello; rm -rf /tmp/important"dangerous_echo() {    local input="$1"    eval "echo $input"   # 세미콜론 뒤의 rm이 실행됨}# 안전: 변수를 항상 따옴표로 감싸기safe_echo() {    local input="$1"    echo "$input"        # 그냥 문자열로 전달}# 위험: 명령어 구성에 변수를 직접 사용# 입력값: "file.txt; cat /etc/passwd"dangerous_view() {    local filename="$1"    cat /logs/$filename   # 경로 조작 가능}# 안전: 파일명만 추출해서 사용safe_view() {    local filename="$1"    # basename으로 경로 조작 방지    local safe_name    safe_name=$(basename "$filename")    cat "/logs/${safe_name}"}

eval 사용 금지

eval은 문자열을 Bash 명령어로 실행합니다. 사용자 입력이 포함된 문자열을 eval에 넘기면 임의의 명령어가 실행됩니다.

# 위험: eval에 외부 입력 포함user_cmd="ls /tmp"eval "$user_cmd"   # 여기까지는 괜찮아 보이지만# 공격자가 이렇게 입력하면user_cmd="ls /tmp; cat /etc/shadow; rm -rf /"eval "$user_cmd"   # 재앙# 안전한 대안들# 1. 배열로 명령어 구성cmd=(ls /tmp)"${cmd[@]}"   # 각 원소가 별도 인수로 전달# 2. 직접 함수 호출case "$action" in    list)   ls "$target_dir" ;;    count)  wc -l "$target_file" ;;    *)      echo "허용되지 않은 작업: $action" >&2; exit 1 ;;esac

eval이 꼭 필요한 경우는 실제로 거의 없습니다. 연관 배열, 함수, case 문으로 대부분 대체할 수 있습니다.

임시 파일 보안 — mktemp와 umask

임시 파일을 직접 만들면 경쟁 조건(race condition)이 생깁니다.

# 위험: 예측 가능한 파일명 (공격자가 먼저 만들 수 있음)TEMP_FILE="/tmp/myapp_$$.tmp"   # $$는 예측 불가하지 않음echo "민감한 데이터" > "$TEMP_FILE"# 안전: mktemp 사용TEMP_FILE=$(mktemp)             # /tmp/tmp.XXXXXXXXXX (충분히 무작위)echo "민감한 데이터" > "$TEMP_FILE"

mktemp는 파일을 생성하는 것과 이름을 결정하는 것을 원자적으로 처리합니다. 공격자가 개입할 틈이 없습니다.

umask를 설정하면 생성되는 파일의 기본 권한을 제한합니다.

# 파일: secure_tempfile.sh#!/usr/bin/env bashset -euo pipefail# 파일 생성 시 다른 사용자가 읽지 못하도록 설정umask 077   # 생성 파일: 600 (rw-------), 디렉토리: 700TEMP_FILE=$(mktemp)TEMP_DIR=$(mktemp -d)cleanup() {    rm -f "$TEMP_FILE"    rm -rf "$TEMP_DIR"}trap cleanup EXIT# 이제 임시 파일은 소유자만 읽고 쓸 수 있음echo "민감한 데이터" > "$TEMP_FILE"ls -la "$TEMP_FILE"# -rw------- 1 user user 15 Apr 24 09:30 /tmp/tmp.AbCdEf

권한 최소화 원칙

스크립트는 필요한 최소한의 권한으로 실행되어야 합니다.

# 파일: least_privilege.sh#!/usr/bin/env bash# root 권한이 필요한지 확인if [[ $EUID -eq 0 ]]; then    echo "경고: root로 실행 중입니다. 가능하면 일반 사용자로 실행하세요." >&2fi# 특정 작업에만 sudo 사용# 전체 스크립트를 root로 실행하는 대신 필요한 명령어만 sudoregular_task() {    echo "일반 작업: 권한 불필요"    ls /tmp}privileged_task() {    echo "권한 필요한 작업"    sudo systemctl restart nginx   # 이 명령어만 sudo}regular_taskprivileged_taskregular_task   # 다시 일반 권한으로

민감 정보 처리 — 로그에 비밀번호 출력 금지

# 파일: sensitive_info.sh#!/usr/bin/env bashset -euo pipefail# 위험: 비밀번호를 환경변수나 명령줄 인수로 전달# mysql -u root -p"$DB_PASSWORD" mydb   # ps로 비밀번호 노출# 안전: 설정 파일로 자격증명 전달connect_db() {    local config_file="$1"    mysql --defaults-file="$config_file" mydb}# 위험: 비밀번호를 로그에 출력# echo "연결: mysql://$DB_USER:$DB_PASSWORD@$DB_HOST"# 안전: 민감 정보 마스킹log_connection() {    local user="$1"    local host="$2"    echo "연결: mysql://${user}:***@${host}"   # 비밀번호 마스킹}# 히스토리에서 민감한 명령어 제외# 명령어 앞에 공백을 붙이면 히스토리에 저장되지 않음 (HISTCONTROL=ignorespace)# 또는 HISTIGNORE 패턴 설정

SUID 스크립트 위험성

SUID(Set User ID) 비트가 설정된 스크립트는 실행자가 아닌 소유자 권한으로 실행됩니다. 쉘 스크립트에 SUID를 설정하는 것은 매우 위험합니다.

# 위험: SUID 쉘 스크립트 (하지 마세요)# chmod u+s /path/to/script.sh   # 절대 하지 마세요# 안전한 대안: sudo 설정으로 특정 명령만 허용# /etc/sudoers에 추가:# user ALL=(ALL) NOPASSWD: /usr/local/bin/specific_command

쉘 스크립트는 해석기(bash)가 실행되는 것이지 스크립트 파일이 직접 실행되는 것이 아닙니다. SUID 비트가 있어도 많은 시스템에서 무시됩니다. 꼭 필요하다면 C로 작성한 래퍼 프로그램을 사용하거나 sudo 정책으로 해결합니다.

실습 — 보안 취약 스크립트를 안전한 버전으로 리팩토링

사용자 입력으로 파일을 검색하는 취약한 스크립트를 안전하게 수정합니다.

# 파일: vulnerable_search.sh (수정 전)#!/bin/bashecho "검색할 키워드를 입력하세요:"read keywordecho "검색할 디렉토리를 입력하세요:"read dir# 위험: 입력 검증 없음, eval 사용, 따옴표 없음result=$(eval "grep -r $keyword $dir")echo "결과: $result"
#!/usr/bin/env bash# 수정: vulnerable_search.shset -euo pipefailreadonly MAX_KEYWORD_LENGTH=100readonly ALLOWED_SEARCH_DIRS=("/tmp" "/home" "/var/log")validate_keyword() {    local keyword="$1"    if [[ ${#keyword} -gt $MAX_KEYWORD_LENGTH ]]; then        echo "오류: 키워드가 너무 깁니다 (최대 ${MAX_KEYWORD_LENGTH}자)." >&2        return 1    fi    # 특수 문자 허용 안 함 (영문, 숫자, 공백, 기본 기호만)    if [[ ! "$keyword" =~ ^[a-zA-Z0-9가-힣\ ._-]+$ ]]; then        echo "오류: 키워드에 허용되지 않은 문자가 포함되어 있습니다." >&2        return 1    fi}validate_directory() {    local dir="$1"    local allowed=false    # 허용된 디렉토리 목록에 있는지 확인    for allowed_dir in "${ALLOWED_SEARCH_DIRS[@]}"; do        if [[ "$dir" == "$allowed_dir" || "$dir" == "$allowed_dir/"* ]]; then            allowed=true            break        fi    done    if ! $allowed; then        echo "오류: 허용되지 않은 디렉토리입니다: $dir" >&2        echo "허용 디렉토리: ${ALLOWED_SEARCH_DIRS[*]}" >&2        return 1    fi    if [[ ! -d "$dir" ]]; then        echo "오류: 디렉토리가 존재하지 않습니다: $dir" >&2        return 1    fi}echo "검색할 키워드를 입력하세요."read -r keywordecho "검색할 디렉토리를 입력하세요 (허용: ${ALLOWED_SEARCH_DIRS[*]})."read -r search_dirvalidate_keyword "$keyword" || exit 1validate_directory "$search_dir" || exit 1echo "검색 중: '$keyword' in '$search_dir'"# eval 없이 안전하게 실행grep -r -- "$keyword" "$search_dir" 2>/dev/null || echo "검색 결과가 없습니다."

수정 전과 후의 차이를 정리하면 다음과 같습니다. eval을 제거하고 직접 명령어를 실행합니다. 키워드 길이와 허용 문자를 검증합니다. 검색 디렉토리를 화이트리스트로 제한합니다. read -r로 백슬래시 해석을 방지합니다. --로 옵션 인젝션을 방지합니다. 보안은 한 번에 완성되는 것이 아닙니다. 위협 모델을 생각하고, 입력을 의심하고, 권한을 최소화하는 습관이 쌓여서 안전한 스크립트가 됩니다.