はじめに (対象読者・この記事でわかること)
本記事は、Linux 系サーバ上で Lighttpd をウェブサーバとして利用し、FastCGI で動作する C 言語のプログラムから system() 関数を用いて外部コマンドを実行したい開発者・運用者を対象としています。
C の標準ライブラリに慣れているが、FastCGI 環境下でのプロセス生成や権限設定、セキュリティリスクについて明確に把握していない方に最適です。この記事を読むことで、以下が実現できます。
- Lighttpd の設定で FastCGI アプリを正しくデプロイする方法
- C コード中で
system()を呼び出す際の安全な書き方と必須のエラーハンドリング - SELinux/AppArmor などの強化モジュールと連携した権限調整手順
プログラムが予期せぬ権限で外部コマンドを実行し、システムを危険にさらすリスクを回避できるようになるでしょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- C 言語での標準入出力と
system()の基本的な使い方 - Linux のユーザー・グループ管理(
chmod,chown)とプロセス権限の概念 - Lighttpd の基本的なインストール手順と設定ファイル(
lighttpd.conf)の構造
FastCGI と system() の関係性・背景
FastCGI は、Web サーバと外部アプリケーション間で高速な IPC(プロセス間通信)を実現するインターフェースです。Lighttpd がリクエストを受け取ると、事前に起動している FastCGI プロセスにデータを渡し、プロセスはそのリクエストを処理して結果を返します。
C 言語で実装された FastCGI アプリは、通常は標準入力/出力を介してデータをやり取りしますが、外部コマンド(例: 画像変換ツール convert、テキスト処理コマンド sed)を呼び出す必要があるケースがあります。その際に system() を使うと、シェル経由でコマンドが実行されますが、以下のリスクが潜在しています。
-
権限昇格リスク
FastCGI プロセスは Lighttpd が起動するユーザー(多くはwww-dataやlighttpd)で実行されます。system()が実行するコマンドは同じユーザー権限になるため、意図しないファイル操作やシステム変更が起こり得ます。 -
入力検証不足によるコマンドインジェクション
HTTP リクエストから取得した文字列を直接system()に渡すと、攻撃者が特殊文字(;や&&)を含めて任意コマンドを実行させることが可能です。 -
SELinux/AppArmor ポリシーとの衝突
SELinux が有効な環境では、system()が呼び出すバイナリが許可リストに無ければ実行がブロックされます。
これらの課題をクリアしたうえで、「安全に、かつ確実に外部コマンドを実行する」手順を以下に示します。
Lighttpd 上で C 言語 FastCGI アプリから system() を安全に呼び出す手順
ステップ1 Lighttpd と FastCGI の基本設定
-
Lighttpd のインストール
bash sudo apt-get update sudo apt-get install lighttpd -
FastCGI モジュールの有効化
bash sudo lighttpd-enable-mod fastcgi sudo lighttpd-enable-mod fastcgi-php # PHP 用の例だが FastCGI は必須 sudo systemctl restart lighttpd -
FastCGI 用の実行ユーザーを限定
lighttpd.confに次のディレクティブを追加して、FastCGI プロセスをfcgiuserという専用ユーザーで実行します。
conf server.modules += ( "mod_fastcgi" ) fastcgi.server = ( "/fcgi-bin" => (( "socket" => "/tmp/fastcgi.socket", "bin-path" => "/var/www/fastcgi/myapp", "check-local" => "disable", "max-procs" => 4, "idle-timeout" => 20, "disable-time" => 0, "chroot" => "/var/www/fastcgi", "uid" => "fcgiuser", "gid" => "fcgiuser" )) ) -
専用ユーザーの作成と権限付与
bash sudo useradd -r -s /usr/sbin/nologin fcgiuser sudo chown -R fcgiuser:fcgiuser /var/www/fastcgi
ポイント:
uid/gidを明示的に設定することで、system()が実行するコマンドは最小権限で走ります。システム全体への影響を最小限に抑える設計です。
ステップ2 C プログラムで安全に system() を呼び出す
2-1. 入力のサニタイズ
HTTP リクエストから取得した文字列 (param) を直接 system() に渡すのは危険です。以下のルールでサニタイズします。
| ルール | 内容 |
|---|---|
| ホワイトリスト | 期待するコマンドやオプションだけを許可する。例: /usr/bin/convert、-resize |
| エスケープ | sh の特殊文字(;, &, |, `, $, >)を除去またはエスケープ |
| 長さ制限 | パラメータ文字列は 256 バイト以内に制限 |
C#include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #define MAX_CMD_LEN 256 /* 許可されたコマンドリスト */ static const char *allowed_cmds[] = { "/usr/bin/convert", "/usr/bin/awk", NULL }; /* コマンドがホワイトリストに入っているか判定 */ int is_allowed(const char *cmd) { const char **p = allowed_cmds; while (*p) { if (strcmp(*p, cmd) == 0) return 1; p++; } return 0; } /* 入力文字列から危険文字を除去 */ void sanitize(char *dst, const char *src) { size_t i, j = 0; for (i = 0; i < strlen(src) && j < MAX_CMD_LEN - 1; i++) { if (isalnum((unsigned char)src[i]) || src[i] == '_' || src[i] == '-' || src[i] == '.' || src[i] == '/') dst[j++] = src[i]; /* それ以外は無視 */ } dst[j] = '\0'; } /* ラッパー関数 */ int safe_system(const char *cmd_path, const char *arg) { char safe_arg[MAX_CMD_LEN]; char full_cmd[MAX_CMD_LEN * 2]; if (!is_allowed(cmd_path)) { fprintf(stderr, "Error: command not allowed\n"); return -1; } sanitize(safe_arg, arg); snprintf(full_cmd, sizeof(full_cmd), "%s %s", cmd_path, safe_arg); return system(full_cmd); }
上記コードは、ホワイトリストで許可したコマンド以外は実行できないようにし、引数に含まれる危険文字は除去しています。これにより、コマンドインジェクションのリスクが大幅に低減します。
2-2. エラーハンドリングとログ出力
system() の戻り値はシェルの終了ステータスです。失敗時に適切にログを残すことで、運用時のトラブルシューティングが容易になります。
Cint ret = safe_system("/usr/bin/convert", user_input); if (ret == -1) { syslog(LOG_ERR, "Attempt to run disallowed command"); // 400 Bad Request を返すなど } else if (WIFEXITED(ret) && WEXITSTATUS(ret) != 0) { syslog(LOG_ERR, "convert exited with status %d", WEXITSTATUS(ret)); // 必要に応じてエラーページを返す }
syslog は daemon ユーザー(=FastCGI プロセス)として動作するため、/var/log/syslog に記録されます。権限が制限されていることから、機密情報が流出するリスクも低減しています。
ステップ3 SELinux / AppArmor のポリシー調整
3-1. SELinux のコンテキスト確認
Bash# FastCGI バイナリのコンテキスト ls -Z /var/www/fastcgi/myapp # 実行したいコマンドのコンテキスト例 ls -Z /usr/bin/convert
myapp_t と convert_exec_t が異なる場合、system() がブロックされます。以下のようにローカルポリシーを追加します。
Bashsudo audit2allow -a -w # ブロックされたイベントを表示 sudo grep convert /var/log/audit/audit.log | audit2allow -M myapp_convert sudo semodule -i myapp_convert.pp
3-2. AppArmor のプロファイル作成
/etc/apparmor.d/usr.bin.fastcgi_myapp に次のように記述します。
/usr/bin/convert ix,
その後、プロファイルをリロードします。
Bashsudo apparmor_parser -r /etc/apparmor.d/usr.bin.fastcgi_myapp
ポイント: 必要最低限の実行許可だけを与えることで、最小権限の原則を徹底できます。
ハマった点やエラー解決
| 発生した問題 | 原因 | 解決策 |
|---|---|---|
system() が「Permission denied」で失敗 |
FastCGI プロセスが fcgiuser で起動し、実行しようとしたコマンドが 755 で無い |
コマンドの実行権限を chmod 750 にし、所有者を fcgiuser に変更 |
SELinux が denied ログを出力 |
ポリシーに execute 権限が無かった |
audit2allow で自動生成したモジュールをインストール |
入力文字列にスラッシュが多すぎて sanitize が途中で切れた |
MAX_CMD_LEN のバッファサイズが不足 |
バッファサイズを 512 に拡張し、snprintf の境界チェックを強化 |
system() の戻り値が 127 |
コマンドパスが間違っているか、PATH が設定されていない |
execve 系と同様にフルパスを指定し、/usr/bin へフルパスで呼び出す |
解決策まとめ
- ユーザー権限の最小化:FastCGI を専用ユーザーで実行。
- ホワイトリスト+サニタイズ:許可されたコマンドと安全な引数だけを受理。
- エラーログの徹底:
syslogで失敗情報を記録し、監視ツールと連携。 - SELinux/AppArmor の調整:最小権限で実行できるようローカルポリシーを追加。
これらを組み合わせることで、安全・確実に外部コマンドを呼び出す環境が構築できます。
まとめ
本記事では、Lighttpd 上で FastCGI の C 言語プログラムから system() を使い外部コマンドを実行する際の 安全な設定手順と実装パターン を解説しました。
- FastCGI プロセスを限定ユーザーで起動し、権限を最小化
- コマンドホワイトリストと入力サニタイズでインジェクション防止
- SELinux/AppArmor ポリシーを調整し、正当な実行を許可
これらを実践すれば、外部コマンド実行によるセキュリティリスクを抑えつつ、必要な処理を確実に行えるようになります。次回は、execve() 系関数を用いた非シェル実行や、非同期実行(fork()+exec())への拡張についても取り上げる予定です。
参考資料
- Lighttpd FastCGI モジュール公式ドキュメント
- SELinux ポリシー作成ガイド
- AppArmor プロファイル作成例
- 《C Programming Language》, Kernighan & Ritchie, 2nd Edition
- Linux 標準ライブラリ
system()関数の使用上の注意
