TT Lab
はじめる
学ぶ 学習パス コース

シェルスクリプティング

敵対的な名前に耐えるスクリプト

TT Labで続きを見る

目標

空白・改行・ダッシュで始まるファイル名と、オプション、そしてインジェクション文字列にまで耐えるスクリプトを書きます。各ルールを、壊れた側と直した側を並べて置いて確認します。

環境

/root/argsの下で作業します。材料は自分で作ります。

mkdir -p /root/args/data && cd /root/args/data
: > "my report.tmp"
: > "$(printf 'two
lines').txt"
: > "-rf"
: > "normal.txt"
ls -b

3つ目が核心です。-rfという名前のファイルを作れます。そして2つ目のように、名前に改行も入ります。ファイル名に入れられない文字は、NULだけです。

作るもの

/root/args/split.txt    따옴표 유무에 따른 인자 수
/root/args/nul.txt      줄 단위와 NUL 단위로 센 개수
/root/args/clean.sh     파일 하나를 이름이 무엇이든 안전하게 지운다
/root/args/wrap.sh      받은 인자를 쪼개지 않고 그대로 넘긴다
/root/args/opts.sh      getopts 로 옵션을 판다
/root/args/search.sh    입력을 값으로만 쓴다
/root/args/report.md    다섯 규칙 정리

採点方法

ステップ3–7では、採点ツールがみなさんのスクリプトを敵対的な入力で直接呼び出します。

wrap.sh    "a b" "c" "d  e" 를 넘겨 3개로 도착하는지
opts.sh    -v -o 값 / 붙여 쓴 -vo 값 / 값 빠진 -o / 모르는 -Z
search.sh  "alpha; touch /tmp/…" 를 넣고 그 파일이 생기는지
clean.sh   "-rf" 라는 이름을 인자로 직접 넘겨 지워지는지

ステップ

  1. 敵対的な名前を4種類作ります。
  2. 同じ値にクォートを付けたときと外したときの引数の数を数えて書きます。
  3. 同じディレクトリを行単位で数えたときと、-print0で数えたときの個数を書きます。2つの数字が違います。
  4. clean.shを書きます。ファイル名を1つ引数として受け取って削除します。存在しないファイルなら、0以外のコードで終わる必要があります。
  5. wrap.shを書きます。受け取った引数ごとにARG:<값>を1行ずつ出力します(プレースホルダーは引数の値です)。
  6. opts.shを書きます。-vと-o <값>を受け取って、残りの引数の数を出力します(プレースホルダーは値です)。出力はverbose=0 out= rest=0の形です。
  7. search.shを書きます。パターンとファイルを受け取って検索します。evalは使いません。
  8. 5つのルールとその根拠をまとめます。

参考

ステップ4でfindを使って名前を取り出すと、先頭に./が付いて問題が隠れます。そのため、採点ツールは名前を引数として直接渡します。現場でもその形が危険です。for f in *や、他のプログラムが渡した名前をそのまま渡すときです。

敵対的な名前を作る

敵対的な名前を4種類作ります。

空白・改行・ダッシュで始まる名前、そして普通の名前を1つ。ls -bで見ると、特殊文字がエスケープされて見えます。

クォート1つで引数の数が変わる

同じ値にクォートを付けたときと外したときの引数の数を数えて書きます。

set -- $fとset -- "$f"のあとで$#を見ると、数字で出ます。なぜそうなるのかも1行書いてください。シェルは展開した後で単語に分割します。

行で数えると間違い、NULで数えると正しい

同じディレクトリを行単位で数えたときと、-print0で数えたときの個数を書きます。2つの数字が違います。

名前に改行が入ったファイルは、行単位で読むと2つとして数えられます。find -print0はNULで区切ります。ファイル名に入れられない唯一の文字なので、安全です。

名前がダッシュで始まるとき

clean.shを書きます。ファイル名を1つ引数として受け取って削除します。存在しないファイルなら、0以外のコードで終わる必要があります。

rm -f "$f"は、名前が-rfだとそれをオプションとして読みます。--が「ここから先は引数」という印です。存在しないファイルには、0以外のコードで終わる必要があります。rm -fは、なくても成功するからです。

受け取った引数をそのまま渡す

wrap.shを書きます。受け取った引数ごとにARG:<값>を1行ずつ出力します(プレースホルダーは引数の値です)。

"$@"は引数をそれぞれ保存し、$*は1つにまとめます。クォートのない$@も、再び分割されます。採点ツールが"a b"と"d e"を渡して、空白が生きているかを確認します。

オプションのパーサーは自作しない

opts.shを書きます。-vと-o <값>を受け取って、残りの引数の数を出力します(プレースホルダーは値です)。出力はverbose=0 out= rest=0の形です。

手作りのパーサーは、-vo 값のように続けて書いた形で破綻します(プレースホルダーは値です)。getopts ":vo:"の先頭のコロンは、サイレントエラーモードを有効にして、:と\?の分岐を自分で処理できるようにしてくれます。最後にshift $((OPTIND - 1))を忘れると、残りの引数の数が狂います。

入力は値であり、コードではない

search.shを書きます。パターンとファイルを受け取って検索します。evalは使いません。

evalは文字列をシェルコードとして実行します。採点ツールがalpha; touch <파일>をパターンとして渡して、そのファイルができるかを確認します(プレースホルダーはファイル名です)。防ぐだけで通常の検索が動かなくなっても、それも失敗です。

5つのルールをまとめる

5つのルールとその根拠をまとめます。

ルールごとに「何を防ぐのか」を1行で書いてください。特にNULを使う理由、つまりファイル名に入れられない文字がそれ1つだけだという事実が抜けると、-print0が習慣としてだけ残ります。