명령 및 인수 주입 방지
신뢰할 수 없는 입력을 인용하고 검증하며 배열로 전달하여 단어 분할과 eval 기반 주입을 제거합니다.
명령 및 인수 주입 방지은(는) CoddyKit의 무료 DevOps Bootcamp 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DevOps Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DevOps Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
Bash에서 주입 공격이 발생하는 이유
Bash는 강력한 접착 언어입니다. 텍스트를 커널, 다른 프로그램, 하위 셸에 직접 전달합니다. 따라서 신뢰할 수 없는 입력이 검증이나 인용 없이 명령에 전달되는 순간 이러한 강력함은 취약점이 됩니다.
거의 모든 Bash 주입 공격은 다음 두 가지 근본 원인에서 발생합니다.
- 단어 분할: 인용하지 않은 변수는 공백 문자(
IFS)를 기준으로 분할되어 하나의 논리적 값이 여러 셸 토큰으로 바뀝니다. - 글로브 확장:
*,?,[와 같은 문자는 명령이 실행되기도 전에 셸에서 확장됩니다.
파일 이름, 사용자 이름, URL 매개변수 또는 환경 변수를 제어할 수 있는 공격자는 이 두 가지를 악용하여 임의의 명령을 실행하거나 파일을 읽거나 권한을 상승시킬 수 있습니다.
이 강의에서는 이러한 취약점이 정확히 어떻게 나타나는지, 그리고 올바른 인용, 입력 검증, 배열 기반 인수 전달을 사용해 무엇보다도 이를 제거하는 방법을 보여 드립니다.
단어 분할: 소리 없이 다가오는 위협
Bash는 인용하지 않은 변수를 만나면 $IFS에 나열된 문자(기본값: 공백, 탭, 줄 바꿈)를 기준으로 값을 분할합니다. 하나의 인수처럼 보이는 값이 여러 인수가 됩니다.
아래 스크립트를 실행하고 공백이 포함된 파일 이름이 rm에 전달되는 두 개의 별도 인수로 바뀌는 모습을 확인해 보십시오.
#!/usr/bin/env bash
# Dangerous: unquoted variable
FILE='important file.txt'
# Create the file so the demo is self-contained
touch "$FILE"
echo "Files before:"
ls
# BUG: rm sees TWO arguments: 'important' and 'file.txt'
# If 'important' does not exist, rm prints an error but continues.
rm $FILE # <-- unquoted, word-split happens here
echo "Files after (unquoted rm):"
ls항상 인용하기: 방어적 Bash의 첫 번째 원칙
단어 분할을 방어하는 가장 간단하고 효과적인 방법은 변수 확장을 항상 큰따옴표로 묶는 것입니다.
"$var"— 정확히 하나의 토큰으로 확장되어 공백, 탭, 줄 바꿈을 보존합니다.'literal'— 작은따옴표는 확장을 전혀 수행하지 않으므로 고정 문자열에 유용합니다.- 단어 분할과 글로브 확장이 명시적으로 필요한 경우가 아니라면
$var를 인용 없이 사용하지 마십시오.
아래 스크립트는 앞선 예제를 안전하게 수정한 버전을 보여 줍니다.
#!/usr/bin/env bash
set -euo pipefail
FILE='important file.txt'
touch "$FILE"
echo 'Files before:'
ls
# SAFE: double-quotes keep the filename as one token
rm "$FILE"
echo 'Files after (quoted rm):'
ls글로브 주입: *가 무기가 될 때
인용하지 않은 변수에는 경로명 확장(글로빙)도 적용됩니다. 사용자 제어 입력에 * 또는 ?가 포함되어 있으면 명령이 실행되기 전에 Bash가 파일 시스템을 기준으로 이를 확장합니다.
대표적인 공격 경로는 다음과 같습니다. 웹 양식에서 PATTERN=*을 설정하고 스크립트가 cp $PATTERN /tmp/leak/을 실행하면 현재 디렉터리의 모든 파일이 복사됩니다.
해결 방법은 동일합니다. 변수를 큰따옴표로 묶으십시오. 인용된 "$PATTERN"은 리터럴 그대로 전달되므로 셸이 글로브 확장을 수행하지 않습니다.
#!/usr/bin/env bash
set -euo pipefail
# Simulate attacker-supplied input
PATTERN='*'
mkdir -p /tmp/safe_demo_src /tmp/safe_demo_dst
touch /tmp/safe_demo_src/secret1.txt /tmp/safe_demo_src/secret2.txt
cd /tmp/safe_demo_src
# UNSAFE: glob expands, copies every file
# cp $PATTERN /tmp/safe_demo_dst/
# SAFE: pattern is treated as a literal filename
cp "$PATTERN" /tmp/safe_demo_dst/ 2>&1 || echo 'No file named literally "*" — attack neutralised'
rm -rf /tmp/safe_demo_src /tmp/safe_demo_dst인용하지 않은 위치 매개변수를 통한 인수 주입
호출자로부터 인수를 받는 스크립트는 주요 주입 대상입니다. 각 위치 매개변수($1, $2, ...)는 사용되는 모든 곳에서 인용해야 합니다.
특히 위험한 방식은 $@ 또는 $*을 인용하지 않고 다른 명령에 전달하는 것입니다.
"$@"— 각 위치 매개변수를 개별적으로 인용된 별도의 단어로 확장합니다. 항상 이 형식을 사용하십시오.$@또는$*을 인용하지 않은 형태 — 단어 분할과 글로브 확장의 영향을 받습니다."$*"— 모든 매개변수를 하나의 단어로 결합하며, 일반적으로 원하는 동작이 아닙니다.
#!/usr/bin/env bash
set -euo pipefail
# Safe wrapper: forward all arguments quoted
grep_wrapper() {
local pattern="$1"
shift
# "$@" preserves each file argument as one token
grep -rn "$pattern" "$@"
}
# Usage: ./script 'error msg' /var/log/syslog '/path with spaces/app.log'
echo 'Searching current script for "safe":'
grep_wrapper 'safe' "$0"eval과 검증되지 않은 입력을 통한 명령 주입
eval은 인수를 셸 코드로 다시 구문 분석합니다. 신뢰할 수 없는 데이터가 eval에 전달되면 임의의 명령을 실행할 수 있습니다.
일반적으로 위험한 방식은 다음과 같습니다.
eval "$user_input"eval echo \$$var(간접 변수 조회)bash -c "$input"을 통해 사용자 데이터를 전달하는 방식
원칙: 신뢰할 수 없는 입력을 eval 또는 bash -c에 절대 전달하지 마십시오. 다음과 같은 안전한 Bash 대안을 사용하십시오.
- 간접 확장:
eval echo \$$varname대신${!varname} - 동적 키-값 조회에는 연관 배열 사용
- 생성된 명령 문자열 대신 함수 사용
#!/usr/bin/env bash
set -euo pipefail
# Simulated attacker-supplied variable name
VARNAME='PATH; echo INJECTED'
# UNSAFE: eval lets attacker run 'echo INJECTED'
# eval "echo \$$VARNAME"
# SAFE: indirect expansion only resolves valid variable names
# First validate that VARNAME is a legal identifier
if [[ "$VARNAME" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "Value: ${!VARNAME}"
else
echo "ERROR: invalid variable name: '$VARNAME'" >&2
exit 1
fi입력 검증: 차단 목록보다 허용 목록
알려진 위험 문자를 거부하는 방식(차단 목록)은 취약합니다. 공격자는 인코딩이나 빠뜨린 문자를 찾아내기 때문입니다. 대신 허용 목록을 사용하여 안전하다고 알고 있는 문자만 허용하십시오.
Bash에서 허용 목록을 사용하는 방법은 다음과 같습니다.
- 정규식 일치:
[[ "$input" =~ ^[A-Za-z0-9_-]+$ ]] - 패턴 일치:
case "$input" in [A-Za-z0-9]*) ... ;; esac - 열거형 확인: 유효한 값으로 구성된 고정 집합과 비교
입력이 스크립트에 들어오는 즉시, 즉 경계에서 검증하고 어떤 명령에도 전달되지 않도록 하십시오.
#!/usr/bin/env bash
set -euo pipefail
validate_username() {
local name="$1"
# Allowlist: only lowercase letters, digits, underscore, hyphen; 1-32 chars
if [[ ! "$name" =~ ^[a-z0-9_-]{1,32}$ ]]; then
echo "ERROR: invalid username '${name}'" >&2
return 1
fi
echo "Username accepted: $name"
}
validate_username 'alice' # OK
validate_username 'bob_smith-2' # OK
validate_username 'root; rm -rf /' # REJECTED
validate_username '../etc/passwd' # REJECTED배열을 사용해 안전하게 인수 전달하기
조건에 따라 플래그를 추가하거나 입력을 반복 처리하는 등 명령을 동적으로 구성해야 할 때는 문자열을 이어 붙이는 대신 Bash 배열을 사용하십시오.
문자열 연결은 모든 구조를 하나로 합치지만, Bash 배열은 각 인수를 셸에서 다시 구문 분석하지 않는 독립적인 요소로 보존합니다.
- 선언:
args=() - 추가:
args+=(--flag "$value") - 실행:
command "${args[@]}"
"${args[@]}"는 각 요소를 "$@"과 정확히 같은 방식으로, 개별적으로 인용된 별도의 단어로 확장합니다.
#!/usr/bin/env bash
set -euo pipefail
# Build a find command safely with an array
BASEDIR='/tmp'
USER_PATTERN='*.log' # could come from user input (validate first!)
MAX_DAYS=7
cmd=(find "$BASEDIR" -type f -name "$USER_PATTERN" -mtime "+${MAX_DAYS}")
# Optionally add -delete only when requested
DELETE=false
if [[ "$DELETE" == 'true' ]]; then
cmd+=(-delete)
fi
echo "Running: ${cmd[*]}"
"${cmd[@]}"-- 구분자: 플래그 주입 방지
올바르게 인용한 인수라도 -로 시작하면 옵션 플래그로 잘못 해석될 수 있습니다. file='-rf .'인 상황에서 rm "$file"을 생각해 보십시오. 인용은 단어 분할을 막지만 rm은 여전히 -rf를 플래그로 해석합니다.
POSIX 관례인 --는 대부분의 GNU/BSD 유틸리티에 옵션의 끝을 알립니다. -- 뒤의 모든 항목은 플래그가 아니라 위치 인수로 처리됩니다.
#!/usr/bin/env bash
set -euo pipefail
# Simulate a dangerous filename supplied by the user
FILENAME='-rf /tmp/safe_demo_target'
mkdir -p /tmp/safe_demo_target
touch /tmp/safe_demo_target/keep_me.txt
echo 'Files before:'
ls /tmp/safe_demo_target/
# UNSAFE (flag injection — do NOT uncomment in real systems):
# rm "$FILENAME"
# SAFE: -- ends option processing; filename is treated literally
rm -- "$FILENAME" 2>&1 || echo "No such file (attack neutralised): $FILENAME"
echo 'Files after:'
ls /tmp/safe_demo_target/
rm -rf /tmp/safe_demo_targetSQL 및 외부 도구를 위한 입력 정제
Bash 스크립트가 데이터베이스 CLI(psql, mysql), 사용자가 제공한 URL을 사용하는 curl 또는 이와 유사한 도구를 호출할 때는 두 가지 추가 계층이 적용됩니다.
- 매개변수화된 쿼리: 사용자 데이터를 SQL 문자열에 절대 삽입하지 마십시오.
psql에서는-v를,curl에서는--data-urlencode를 통해 값을 전달하십시오. - 데이터와 코드를 분리: 리터럴 형식 문자열과 함께
printf를 사용하고, 사용자 입력을 형식 문자열로 사용하지 마십시오.
아래 예제는 사용자가 제공한 값을 SQL 텍스트에서 완전히 분리한 채 PostgreSQL을 안전하게 조회합니다.
#!/usr/bin/env bash
set -euo pipefail
# Validate first: only allow alphanumeric usernames
USERNAME="${1:-alice}"
if [[ ! "$USERNAME" =~ ^[a-z0-9_]{1,32}$ ]]; then
echo 'ERROR: invalid username' >&2
exit 1
fi
# UNSAFE:
# psql -c "SELECT * FROM users WHERE name = '$USERNAME';"
# SAFE: pass value as a psql variable, never inside the SQL text
# psql -v username="$USERNAME" -c 'SELECT * FROM users WHERE name = :username;'
# Demonstrate the principle with printf (safe format string usage)
printf 'Query would use username: %s\n' "$USERNAME"보안 강화 점검 목록: 모든 내용 통합하기
운영 환경 수준의 안전한 Bash 스크립트는 이 강의의 모든 기법을 일관되고 다층적인 방어로 결합합니다. 다음은 최소한의 완전한 보안 강화 템플릿입니다.
set -euo pipefail— 오류가 발생하면 종료하고, 설정되지 않은 변수를 오류로 처리하며, 파이프라인 오류를 전파합니다.- 진입 시 검증 — 외부 입력이 어떤 명령에도 전달되기 전에 모두 허용 목록으로 검증합니다.
- 모든 항목 인용 —
"$var","$@","${array[@]}"을 사용하며, 분할이 필요한 경우가 아니면 예외를 두지 않습니다. - 동적 명령 구성에는 배열을 사용합니다.
- 사용자가 제공한 파일 이름이나 문자열을 전달할 때 인수 앞에
--를 붙입니다. - 신뢰할 수 없는 데이터와 함께
eval을 절대 사용하지 말고, 간접 조회에는${!var}를 사용합니다. - 권한 제한 — 필요한 최소 권한으로 스크립트를 실행하고, 사용자 입력을 받는 스크립트 내부에서는
sudo사용을 피합니다.
#!/usr/bin/env bash
# Hardened template — safe argument injection prevention
set -euo pipefail
IFS=$'\n\t'
#--- 1. Validate inputs at the boundary ---
SEARCH_DIR="${1:-}"
PATTERN="${2:-}"
[[ -z "$SEARCH_DIR" || -z "$PATTERN" ]] && { echo 'Usage: script <dir> <pattern>' >&2; exit 1; }
[[ ! "$SEARCH_DIR" =~ ^[A-Za-z0-9/_.-]+$ ]] && { echo 'ERROR: unsafe directory path' >&2; exit 1; }
[[ ! "$PATTERN" =~ ^[A-Za-z0-9._-]+$ ]] && { echo 'ERROR: unsafe pattern' >&2; exit 1; }
#--- 2. Build command with an array ---
cmd=(find -- "$SEARCH_DIR" -type f -name "$PATTERN")
#--- 3. Execute — no string interpolation, no eval ---
echo "Executing: ${cmd[*]}"
"${cmd[@]}"지식 확인: 인용 및 주입 방지
이 강의의 핵심 개념을 얼마나 이해했는지 확인해 보십시오.
강의 요약: 명령 및 인수 주입 방지
안전한 Bash 입력 처리를 위한 완전한 방어 도구 모음을 살펴보았습니다.
- 단어 분할과 글로브 확장은 안전하지 않은 변수를 주입 공격 경로로 바꾸는 근본적인 메커니즘입니다.
- 모든 변수를 큰따옴표로 묶어(
"$var","$@","${arr[@]}") 두 위협을 모두 차단합니다. - 인수를 전달할 때는
"$@"을 사용하고, 인용하지 않은$@이나$*은 절대 사용하지 마십시오. - 사용자가 제공한 파일 이름 앞에
--를 붙여 플래그 주입을 방지합니다. - 정규식 검사(
[[ $v =~ ^pattern$ ]])를 사용해 모든 외부 입력을 허용 목록으로 검증한 후 명령에 전달합니다. - 배열(
cmd+=()→"${cmd[@]}")로 동적 명령을 구성하고, 문자열 연결은 절대 사용하지 않습니다. eval과bash -c "$input"을 제거하고, 안전한 간접 확장에는${!varname}을 사용합니다.- 항상
set -euo pipefail과IFS=$'\n\t'로 스크립트를 시작하여 보안 강화의 기본 상태를 설정합니다.
모든 스크립트의 첫 줄부터 이러한 방식을 일관되게 적용하면 주입 유형 취약점에 대한 Bash의 공격 표면을 거의 0에 가깝게 줄일 수 있습니다.
자주 묻는 질문
“명령 및 인수 주입 방지” 강의는 무료인가요?
네 — “명령 및 인수 주입 방지” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DevOps Bootcamp 강의 전체를 잠금 해제할 수 있습니다. DevOps Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“명령 및 인수 주입 방지”에서 뭘 배우나요?
신뢰할 수 없는 입력을 인용하고 검증하며 배열로 전달하여 단어 분할과 eval 기반 주입을 제거합니다. 브라우저에서 직접 실행하는 실습 코드로 DevOps Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
DevOps Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 DevOps Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“명령 및 인수 주입 방지” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 DevOps Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 DevOps Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.