Статический анализ и аудит с ShellCheck
Встраивайте ShellCheck в этап проверки безопасности и интерпретируйте его результаты, укрепляя каждый скрипт
«Статический анализ и аудит с ShellCheck» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.
Что такое ShellCheck и почему это важно
ShellCheck — это статический анализатор с открытым исходным кодом для скриптов оболочки. Он разбирает исходный код Bash (а также POSIX sh, dash и ksh), не выполняя его, и сообщает об ошибках, небезопасных конструкциях, проблемах переносимости и нарушениях стиля; каждому сообщению присваивается уникальный код правила, например SC2086.
В защищённом конвейере ShellCheck выступает как обязательный шлюз: ни один скрипт не выпускается, пока не пройдёт проверку. Это важно, потому что:
- Многие уязвимости оболочки (разбиение слов, внедрение, незакавыченные подстановки) незаметны при проверке штатных сценариев, но проявляются при вводе под контролем злоумышленника.
- ShellCheck обнаруживает такие классы ошибок до выполнения и без дополнительных затрат.
- Он объясняет, почему каждый шаблон опасен, благодаря чему со временем повышается осведомлённость команды.
Установите его в любой системе:
# Debian / Ubuntu
sudo apt-get install shellcheck
# macOS (Homebrew)
brew install shellcheck
# From source via Cabal (any platform)
cabal update && cabal install ShellCheck
# Verify
shellcheck --versionПервый запуск ShellCheck
Самый простой вызов — shellcheck <script>. ShellCheck читает строку shebang, чтобы определить диалект оболочки, а затем выводит результаты в stdout.
Каждое сообщение содержит:
- имя файла и номер строки — точное расположение
- уровень серьёзности —
error,warning,infoилиstyle - код SC — стабильный идентификатор правила, который можно найти в документации или подавить
- объяснение для человека — сообщает, что неверно и часто как это исправить
Запустите приведённый ниже скрипт и посмотрите на вывод, который сформировал бы ShellCheck:
#!/usr/bin/env bash
# demo_bad.sh — intentionally flawed for ShellCheck demonstration
FILE=$1
if [ $FILE == '' ]; then
echo "No file given"
fi
cat $FILE | grep 'error' | wc -lИнтерпретация вывода ShellCheck и кодов SC
Для скрипта из предыдущего раздела ShellCheck сформировал бы примерно такие сообщения:
SC2086(предупреждение) — Заключите в двойные кавычки, чтобы предотвратить подстановку шаблонов и разбиение слов — для$FILEв[ $FILE == '' ]и вcat $FILE.SC2039/SC3010(информация) — == в [ ] является расширением Bash; для POSIX используйте =.SC2002(стиль) — Бесполезный cat. Вместо cat file | cmd рассмотрите cmd < file.
Каждому коду SC соответствует страница вики по адресу https://www.shellcheck.net/wiki/SCxxxx с обоснованием и исправленным примером.
Исправленная версия этого скрипта:
#!/usr/bin/env bash
# demo_fixed.sh — ShellCheck-clean
FILE="$1"
if [ -z "$FILE" ]; then
echo "No file given" >&2
exit 1
fi
grep -c 'error' "$FILE"Уровни серьёзности и требуемые действия
ShellCheck классифицирует каждое сообщение по уровню серьёзности. В шлюзе безопасности следует действовать так:
- error — почти наверняка ошибка или уязвимость. Заблокируйте сборку. Немедленно исправьте. Пример:
SC2148— отсутствует shebang,SC2070— незакавыченный$?. - warning — шаблон высокого риска, который часто можно использовать для атаки. Заблокируйте сборку. Исправьте или явно обоснуйте подавление. Пример:
SC2086— незакавыченная переменная. - info — сегодня, вероятно, всё работает правильно, но конструкция хрупкая или непереносимая. Исправьте в том же PR, если проблема не признана выходящей за рамки.
- style — косметическое замечание или рекомендация POSIX. Рекомендуется, но необязательно в проекте на чистом Bash.
Используйте --severity=warning, чтобы завершать работу с ненулевым кодом только при наличии предупреждений и сообщений более высокого уровня — это стандартный порог шлюза безопасности:
#!/usr/bin/env bash
# gate.sh — fail CI on errors and warnings only
shellcheck --severity=warning scripts/*.sh
echo "ShellCheck exit code: $?"Интеграция ShellCheck в качестве шлюза безопасности CI
Шлюз безопасности полезен только тогда, когда он обязателен и автоматизирован. Приведённый ниже шаблон оборачивает ShellCheck в шаг CI, который:
- Находит каждый файл
.shв репозитории. - Запускает ShellCheck с параметром
--severity=warningи машиночитаемым выводом в формате JSON. - Завершает конвейер с ошибкой (
exit 1), если найдено хотя бы одно сообщение. - Выводит сводку, чтобы разработчики могли обработать результаты, не покидая журнал CI.
Поместите этот файл в репозиторий и вызывайте его из конвейера CI (GitHub Actions, Jenkins, GitLab CI и т. д.):
#!/usr/bin/env bash
# ci/shellcheck_gate.sh
set -euo pipefail
SCRIPTS=$(find . -name '*.sh' -not -path './.git/*')
FAILED=0
for script in $SCRIPTS; do
echo "==> Checking: $script"
if ! shellcheck --severity=warning --format=tty "$script"; then
FAILED=1
fi
done
if [ "$FAILED" -eq 1 ]; then
echo "[GATE] ShellCheck found warnings or errors. Build blocked." >&2
exit 1
fi
echo "[GATE] All scripts passed ShellCheck."Семейство SC2086: незакавыченные подстановки переменных
SC2086 — самое распространённое сообщение ShellCheck и одна из наиболее часто используемых уязвимостей оболочки: неза>кавыченные подстановки переменных.
Если переменная не заключена в двойные кавычки, оболочка выполняет для её значения разбиение слов (по IFS) и подстановку шаблонов. Злоумышленник, контролирующий переменную, может внедрить дополнительные аргументы, вызвать обход файловой системы или заставить команды получить неожиданные операнды.
Классический опасный шаблон:
#!/usr/bin/env bash
# Attacker sets: FILENAME="important.txt /etc/passwd"
FILENAME="$1"
# UNSAFE — word splitting turns this into two args
rm $FILENAME
# SAFE — double quotes prevent splitting
rm "$FILENAME"
# Arrays are the right tool for lists
FILES=("$@")
rm -- "${FILES[@]}"Обнаружение риска внедрения команд с помощью SC2046 и SC2035
Два менее известных, но критически важных правила касаются внедрения команд через вывод подоболочки:
SC2046— Заключите это в кавычки, чтобы предотвратить разбиение слов и подстановку шаблонов внутри$(…). Если вывод подоболочки используется без кавычек, любой пробел или символ шаблона в выводе превращается в токен оболочки.SC2035— Используйте./*.shвместо*.sh, чтобы имена файлов, начинающиеся с-, не интерпретировались как параметры (классический вектор внедрения аргументов).
Конкретный сценарий эксплуатации и исправление:
#!/usr/bin/env bash
# SC2046 example — output of find fed unquoted to chmod
# If a filename contains spaces, extra arguments appear
# UNSAFE
chmod 600 $(find /secrets -name '*.key')
# SAFE — use a while-read loop or xargs with -0
find /secrets -name '*.key' -print0 \
| xargs -0 chmod 600
# SC2035 example
# UNSAFE — a file named '-rf' would be passed as an option
rm *.sh
# SAFE
rm -- ./*.shИспользование формата вывода JSON для автоматизации
ShellCheck поддерживает несколько форматов вывода с помощью --format:
tty(по умолчанию) — вывод в терминале, удобный для чтенияjson— машиночитаемый формат; идеален для панелей мониторинга, пользовательских блокировок или загрузки на платформы SASTgcc— совместим с инструментами, разбирающими формат ошибок GCC (IDE, Vim/Emacs)checkstyle— формат XML, который обрабатывает подключаемый модуль Checkstyle для Jenkins
Формат JSON позволяет создавать автоматические политики, например блокировать сборку только по определённым кодам SC или объединять результаты для большой кодовой базы в один отчёт о безопасности.
#!/usr/bin/env bash
# Emit JSON and filter for only error-severity findings using jq
shellcheck --format=json scripts/deploy.sh \
| jq '[.[] | select(.level == "error")]'
# Count distinct SC codes across all scripts
find . -name '*.sh' -print0 \
| xargs -0 shellcheck --format=json 2>/dev/null \
| jq '[.[] | .code] | group_by(.) | map({code: .[0], count: length}) | sort_by(-.count)'Корректное подавление ложных срабатываний
Безусловное отключение ShellCheck сводит на нет его предназначение. Правильный подход — это точечное подавление с документированным обоснованием, которое затрагивает только конкретную строку или блок, где сообщение действительно неприменимо.
Три механизма подавления:
- Отключение в строке —
# shellcheck disable=SC2086в строке перед проблемным кодом. Затрагивает только эту строку. - Отключение и включение блока — окружите раздел директивами
# shellcheck disable=…и# shellcheck enable=…. - Директива на уровне файла — поместите
# shellcheck disable=…в начало файла (оправдано редко; укажите причину).
Каждое подавление должно сопровождаться комментарием, объясняющим, почему сообщение является ложным срабатыванием:
#!/usr/bin/env bash
# deploy.sh
# Legitimate suppression: $DEPLOY_ARGS is intentionally word-split
# because it is a pre-validated list of flags from a trusted config file.
# shellcheck disable=SC2086
exec deploy-tool $DEPLOY_ARGS
# Block suppression for a section that generates dynamic code
# shellcheck disable=SC2016
VARS='$HOME $PATH $USER'
echo "Unexpanded vars: $VARS"
# shellcheck enable=SC2016Настройка ShellCheck через .shellcheckrc
Для общих настроек проекта ShellCheck ищет .shellcheckrc, начиная с каталога скрипта и поднимаясь вверх до /. Это позволяет не повторять параметры при каждом вызове и сохраняет простоту скриптов шлюза.
Полезные директивы в .shellcheckrc:
shell=bash— переопределяет определение диалекта (полезно для файлов без shebang)enable=all— включает необязательные проверки (например,avoid-nullary-conditions,require-variable-braces)disable=SC2059— подавление на уровне проекта для обоснованного исключенияexternal-sources=true— отслеживает и проверяет директивыsource/.
# .shellcheckrc — project root
shell=bash
enable=all
external-sources=true
# SC2312: consider invoking this command separately to avoid masking its
# return value — suppressed project-wide because we use set -e.
# Rationale: errexit already aborts on failure; masking risk is mitigated.
disable=SC2312Защищённый скрипт от начала до конца: до и после
Лучший способ усвоить сообщения ShellCheck — переработать реалистичный скрипт из состояния с ошибками в чистый и защищённый вариант. Приведённый ниже скрипт создаёт резервную копию каталога и был написан без учёта безопасности. Он нарушает как минимум пять различных правил ShellCheck.
Изучите обе версии. Вариант после проходит shellcheck --severity=warning без директив подавления и значительно безопаснее при вводе под контролем злоумышленника:
#!/usr/bin/env bash
# BEFORE — multiple ShellCheck violations
DEST=$1
SRC=$2
DATE=`date +%Y%m%d`
if [ ! -d $DEST ]; then
mkdir $DEST
fi
cp -r $SRC $DEST/$DATE
echo Done
#!/usr/bin/env bash
# AFTER — ShellCheck-clean and hardened
set -euo pipefail
DEST="${1:?Usage: backup.sh <dest> <src>}"
SRC="${2:?Usage: backup.sh <dest> <src>}"
DATE=$(date +%Y%m%d)
if [ ! -d "$DEST" ]; then
mkdir -p -- "$DEST"
fi
cp -r -- "$SRC" "$DEST/$DATE"
echo 'Done' >&2Проверка знаний: ShellCheck как шлюз безопасности
Проверьте, насколько хорошо Вы понимаете роль ShellCheck как шлюза безопасности.
Итоги: статический анализ как шлюз безопасности
В этом уроке Вы узнали, как сделать ShellCheck обязательным шлюзом безопасности в рабочем процессе Bash:
- ShellCheck выполняет статический анализ без запуска скриптов, обнаруживая ошибки заключения в кавычки, риски внедрения и небезопасные шаблоны до выполнения.
- Каждое сообщение содержит код SC (например,
SC2086), связанный с подробной документацией и рекомендациями по исправлению. - Шкала серьёзности —
error,warning,info,style— позволяет настроить шлюз:--severity=warningявляется рекомендуемым порогом безопасности. - Используйте машиночитаемый вывод (
--format=json) для автоматизации отчётности, отслеживания тенденций и интеграции с SAST. - Подавляйте сообщения только в необходимых случаях: всегда ограничивайте подавление одной строкой, всегда документируйте причину в комментарии и никогда не отключайте проверки глобально без обоснования в
.shellcheckrc. - Сочетайте ShellCheck с
set -euo pipefail, явным заключением в кавычки, терминаторами-- argumentи проверкой ввода для многоуровневой защиты.
Скрипт, проходящий ShellCheck, не становится автоматически безопасным, но скрипт, не проходящий ShellCheck, никогда не должен попадать в рабочую среду.
Часто задаваемые вопросы
Урок «Статический анализ и аудит с ShellCheck» бесплатный?
Да — полный текст урока «Статический анализ и аудит с ShellCheck» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.
Чему я научусь в уроке «Статический анализ и аудит с ShellCheck»?
Встраивайте ShellCheck в этап проверки безопасности и интерпретируйте его результаты, укрепляя каждый скрипт Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DevOps Bootcamp?
Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Статический анализ и аудит с ShellCheck»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DevOps Bootcamp?
Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Предотвращение внедрения команд и аргументов
- Безопасная работа с секретами и чистота окружения
- Выполнение с минимальными привилегиями и дисциплина sudo
- Статический анализ и аудит с ShellCheck