Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

外部プロセスとしてシェルスクリプトを起動する方法

任意の処理系から外部プロセスとしてシェルスクリプトを起動したい場合、大きく以下の2つの方法が考えられます。

(1) shの引数にシェルスクリプトを指定する
/bin/sh foobar.sh のように、shの引数にシェルスクリプトを指定して起動する。
  • shに与えるパラメタはファイル・DB・環境変数から与えたりすることが多い。
  • shに与えるパラメタは多くの場合ユーザ入力に依らないので、OSコマンドインジェクションを気にせず実行できる。
(2) シェルスクリプトを直接指定する
/path/to/foobar.sh parameterのように、シェルスクリプトchmod a+xして起動する。
  • shに与えるパラメタはコマンドライン引数で指定する。
  • shに与えるパラメタはがユーザ入力に依ることがある。そのためOSコマンドインジェクションが起こりにくいよう、コマンド部分と引数部分を明確に分けて指定する。従って、コマンド部分に/bin/sh foobar.shとか指定すると"/bin/sh foobar.sh"というファイルを探しに行って実行に失敗してしまう。

(1)(2)ともに、開発環境がWindowsで本番環境がUnix系OS(よくある)とかだと、開発環境でどうやってシェルスクリプトを実行するか?という話になるわけです。これまた大きく4パターン(現在生きているのは3パターン)が考えられます。

(a) 開発環境に追加で本番環境と同じUnix系OSの実行環境を用意する
  • 実機にせよVMにせよ、追加のマシンが必要
  • 開発環境と本番環境の挙動差を気にしなくてよい
  • シェルスクリプトから起動されるミドルウェアも普通にセットアップしてしまえばよい
  • 簡単なメンテなら誰でもできるから開発環境をWindowsにしているのに、そのメリットが薄れる
  • 本番環境がAIXとかだと開発環境を大量に用意するのがしんどい
  • シェルスクリプトchmod a+xしてしまえば(1)も(2)も問題なく実行できる
(b) Git for Windows付属のBashMinGW)を使う
  • 追加のマシンが不要
  • WindowsVM上に存在する場合(AWS Workspacesなど)でも利用できる
  • シェルスクリプトから起動される各種ミドルウェアをGit for Windowsの環境上にセットアップするのは難しい
  • フォルダ構成がFHSとだいぶ違う。デフォルトではそもそも/bin/shがない(/bin/bashしかない)
  • (1)はGit for Windowsインストール先のbash.exeを指定すれば問題ないが、パスが開発環境と本番環境で異なる
  • (2)はGit for Windowsインストール時に関連づけされるが、起動方法によっては動作しない(cmd.exeからは関連付けで起動できるが、処理系からシェルを経由せずに実行しようとすると起動できない)
(c) WSLのshを使う
  • 追加のマシンが不要
  • WindowsVM上に存在する場合(AWS Workspacesなど)では使えない
  • フォルダ構成がFHSに従っている
  • initがsystemdでないのでシェルスクリプト中でsystemd関連のコマンド(systemctlなど)を呼ぶ場合は使えない
  • シェルスクリプトから起動される各種ミドルウェアがWSLに対応していない可能性がある
  • (1)はC:\Windows\System32\bash.exeを指定すれば問題ないが、パスが開発環境と本番環境で異なる
  • (2)はcmd.exeからは関連付けで起動できるが、処理系からシェルを経由せずに実行しようとすると起動できない。また関連づけを手動で設定する必要がある*1
  • (2)の変種として、.shと同名の.batを用意して、そこから.shを呼び出す方法がある。.batの内容はたとえば以下のような感じ。PowerShellを利用しているが、実行した.batの名前を組み立てられればPowerShellを使わなくても同じことができると思われる。
    @setlocal enableextensions enabledelayedexpansion & set "THIS_PATH=%~f0" & PowerShell.exe -Command "& (iex -Command ('{$PSCommandPath=\"%~f0\"; $PSScriptRoot=\"%~dp0"; #' + ((gc '!THIS_PATH:'=''!') -join \"`n\") + '}'))" %* & exit /b !errorlevel!
    $ShPath = $PSCommandPath.substring(0,$PSCommandPath.length-4) + ".sh"
    wsl ("/mnt/"+[System.Char]::ToLower($ShPath[0])+$ShPath.Substring(2,$ShPath.Length-2) -replace '\\', '/') $args;
    exit $LASTEXITCODE
    
(d) Services for UNIX (SFU)を使う
  • Windows Server 2008のころはよく使われたが、Windows Server 2012R2以降SFUが死にたえたため現在は使えない
  • 本番環境がAIXとかだとデフォルトのシェルがSFUと同じkshなので都合が良かった

シェルスクリプトに渡す引数をサニタイズ

シェルスクリプトに渡した引数を、スクリプト中で単純に$@で参照すると、OSコマンドインジェクションを招くおそれがある。

$ cat sanitize4.sh
#!/bin/bash

ash -s <<EOF
ls $@
EOF

$ ./sanitize4.sh "ls;ls /usr"
ls: cannot access 'ls': No such file or directory
bin  games  include  lib  lib32  lib64  libexec  libx32  local  sbin  share  src

対策として、シェルがbashならprintf '%q 'を使って文字列のエスケープができる。参考: https://stackoverflow.com/a/56688189/3902663

$ cat sanitize1.sh
#!/bin/bash

printf -v args_q '%q ' "$@"
ash -s <<EOF
ls '$args_q'
EOF

$ ./sanitize1.sh "ls;ls /usr"
ls: cannot access 'ls\;ls\ /usr ': No such file or directory
$

ワンライナーにもできる。

$ cat sanitize2.sh
#!/bin/bash

printf 'ls /%q ' "$@" | bash -s

$ ./sanitize2.sh "usr"
bin  games  include  lib  lib32  lib64  libexec  libx32  local  sbin  share  src
$ ./sanitize2.sh "usr;ls /usr"
ls: cannot access '/usr;ls /usr': No such file or directory

ssh$SSH_ORIGINAL_COMMANDなら、わざわざエスケープしなくても大丈夫みたい。コマンド内に特殊文字が含まれていても解釈せずに実行してくれた。

# cat authorized_keys
command="ls /$SSH_ORIGINAL_COMMAND" ssh-rsa AAAAB....

$ ssh root@xxx.xxx.xxx.xxx -i ~/.ssh/com "tmp;hostname"
ls: /tmp;hostname: No such file or directory

ただし特殊文字の解釈を止めるだけで、/../とかはそのまま解釈されてしまう。そのため、ディレクトリトラバーサルは上記の方法では防げない。

ディレクトリトラバーサルに対してはchrootなどが対策になると思うが、何も考えずにchrootしてもシェルの内部コマンドしか使えない環境になってしまうのでそう簡単には使えない。

CPU goes 100% when you read .HAR file with ConvertFrom-JSON

Background

In Chrome DevTools, you can export the contents in the Network tab into a JSON file (.HAR file).

Chrome cannot export the contents of the spreadsheet in the Network tab into .csv or .xlsx format. If you want to get these contents, you have to read the .HAR file and convert it to .csv format.

Problem

When you read a .HAR file with ConvertFrom-JSON cmdlet in PowerShell, the CPU which runs powershell.exe goes 100%, and the console which runs powershell.exe respond to Ctrl-C.

PS > Get-Content .\new-tab-page.har | ConvertFrom-Json

Cause

.HAR files that Chrome exported are encoded with UTF-8. ConvertFrom-Json falls into a kind of infinity loop when it reads inappropriately encoded strings.

Solution

Explicitly specify the encoding of the .HAR file when you read it.

PS > Get-Content .\new-tab-page.har -Encoding UTF8 | ConvertFrom-Json

log
---
@{version=1.2; creator=; pages=System.Object[]; entries=System.Object[]}

Overwriting existing file never change creation time in NTFS

Overwriting existing file like 1..10 > file.txt, 1..10 | Out-File file.txt, or 1..10 | Set-Content file.txt, never changes creation time in NTFS.

You have to delete the file explicitly like rm file.txt; 1..10 > file.txt, or change creationTime explicitly like Set-ItemProperty .\file.txt -name CreationTime -Value (Get-Date).