Skip to main content

Pythonのコマンドインジェクション:例と防止策

著者
Headshot of Rubaiat Hossain

Rubaiat Hossain

feature python command injection

2023年12月21日

0 分で読めます

Pythonはシンプルで汎用性が高いことで知られていますが、開発中にあなたやチームメンバーがセキュリティのベストプラクティスを怠ると、Pythonプログラムの安全性を確保するのは難しくなります。

また、Pythonアプリケーションの開発では、ライブラリやその他のオープンソースプロジェクトを利用することも多いでしょう。しかし、こうしたリソースが新たなセキュリティ上の問題を引き起こし、プログラムをコマンドインジェクションなどの攻撃に対して脆弱にする可能性があります。この重大なセキュリティ上の欠陥は、Pythonアプリケーションに深刻な被害をもたらしかねません。コマンドインジェクションにより、アプリケーション上で許可されていないコマンドが実行され、データ侵害やシステムの侵害、その他の悪意ある活動につながる恐れがあります。

この記事では、コマンドインジェクションについて詳しく解説し、プログラムでこの脆弱性がどのように発生するかを紹介します。また、Pythonアプリをコマンドインジェクション攻撃から守るための、一般的なセキュリティのベストプラクティスも学べます。

コマンドインジェクションとは?

コマンドインジェクションとは、攻撃者が脆弱なプログラムに任意のシステムコマンドを注入して実行できる状態を指します。この脆弱性は通常、アプリケーションが安全でないユーザーデータ(フォーム、Cookie、HTTPヘッダーなど)をシステムシェルに渡すことで発生します。

サニタイズされていないユーザー入力をシステムコマンドに渡すなど、さまざまな状況でコマンドインジェクションの脆弱性が生じます。アプリケーションがユーザー入力をコマンドラインに直接渡すと、攻撃者は悪意ある引数を注入できます。たとえば、機械学習ライフサイクルプラットフォームのMLflowの一部バージョンで見つかったコマンドインジェクションの脆弱性が挙げられます。この脆弱性は、backend.pyファイル内のpredict()メソッドに対するチェックが不十分であることに起因します。

また、アプリケーションがユーザー入力などのさまざまなパラメーターに基づいて、コマンド文字列を動的に組み立てることもあります。組み立ての過程で適切なチェックや検証が行われないと、攻撃に悪用される可能性があります。

残念ながら、システムコマンドの影響を正しく理解するのは簡単ではありません。システムコマンドの誤用や設定ミスは、コマンドインジェクションの脆弱性を招きやすくなります。たとえば、並列分散ディープラーニングプラットフォームであるPaddlePaddleの影響を受けるバージョンでは、fs.py内でos.systemメソッドを介したコマンドインジェクションの脆弱性があります。

コマンドインジェクションの影響は深刻です。攻撃者は不正にアクセスし、機密データを持ち出したり、システムを破壊したりする可能性があります。このような侵害は、組織に金銭的損失や評判の失墜、法的責任をもたらすほか、機密情報を盗まれたユーザーにも計り知れない被害を与える恐れがあります。

Pythonにおけるコマンドインジェクションの理解と修正

コマンドインジェクションは、アプリケーションセキュリティに対する最も深刻な脅威の一つです。Pythonアプリケーションをコマンドインジェクションから守るには、こうした脆弱性を深く理解し、潜在的な攻撃を防ぐための予防策を講じる必要があります。

コマンドインジェクションにつながる一般的な脆弱性

以下では、Pythonでコマンドインジェクションにつながる一般的な脆弱性をいくつか紹介します。

ユーザーが制御する入力の悪用

ユーザーが制御する入力とは、フォームデータやURLパラメーター、コマンドラインからの入力など、ユーザーが直接提供するデータや値です。アプリケーションが適切に検証せずにこれらの入力を処理し、システムコマンドの実行にそのまま使用すると、コマンドインジェクション攻撃に対して脆弱になります。

次のPythonの例を見てみましょう。

# Vulnerability: Directly using user-controlled input to execute a command.
command = input("Enter a command to execute: ")
os.system(command)

このコードは、inputメソッドを使ってユーザーにコマンドを入力するよう求めます。このユーザーが制御する入力は、検証やチェックを一切行わずにos.systemメソッドへ直接渡され、実行されます。つまり、ユーザーの入力はすぐにシステムのコマンドインタープリターによって処理・実行されます。

悪意のあるコマンドとディレクトリ一覧を表示し、PythonのコマンドインジェクションをテストするUbuntuターミナル

この簡単な例では、コードをテストするために基本的なシェルコマンドを使っています。しかし攻撃者は、権限昇格やデータ損失など、望ましくない事態を引き起こす危険なコマンドを使う可能性があります。

システムコマンドとsubprocessモジュールの安全でない使用

Pythonのシステムコマンドとsubprocessモジュールを使うと、シェルコマンドを実行でき、基盤となるオペレーティングシステムと柔軟に連携できます。しかし、安全に使用しないと、特にユーザーが制御する入力と組み合わせた場合に、コマンドインジェクションの脆弱性を生む可能性があります。

次の脆弱なコードを例に見てみましょう。

# Vulnerability: Using subprocess with shell=True and unsanitized user input.
command = input("Enter a command for subprocess: ")
subprocess.run(command, shell=True)

ここでは、shell=Trueを有効にしたsubprocess.runメソッドを使って、ユーザー入力を直接実行しています。shell=Trueを指定するとシステムシェルがコマンドを実行するため、コマンドの連結など、悪意ある操作につながる可能性があります。

echoコマンドとlsコマンドを実行し、Pythonのsubprocessを使ったコマンドインジェクションを示すUbuntuのターミナル。

ユーザー入力を検証またはサニタイズしないことで、スクリプトはコマンドインジェクション攻撃にさらされ、攻撃者にシステムシェルの権限をそのまま渡してしまいます。

コマンドを動的に組み立てることに伴うリスク

特にユーザー入力などの可変データを組み込む必要がある場合、Pythonプログラムがシステムコマンドを動的に組み立てて実行することは珍しくありません。しかし、動的な組み立てに適切なセキュリティ対策が施されていないと、コマンドインジェクションの脆弱性を招きます。

次のPythonコードを見てみましょう。

# Vulnerability: Dynamically constructing commands without sanity checks.
user_input = input("Enter a parameter: ")
command = f"echo Printing {user_input}"
os.system(command)

このコードはユーザーにパラメーターの入力を求め、その入力を直接組み込んだコマンド文字列を作成します。このコマンドはユーザーの入力を表示することを目的としています。しかし、コマンドに埋め込む前にユーザー入力を検証またはサニタイズしていないため、コマンドインジェクションに対して脆弱です。

「Hello; ls -l」を受け取り、「Hello」とディレクトリ一覧を出力するPythonデモを表示したUbuntuターミナル。

攻撃者は、コマンド区切り文字や制御演算子を含むペイロードを入力して、この脆弱性を悪用できます。たとえば、ユーザーがHello; ls -lと入力すると、まず"Hello"が表示され、その後セミコロンがコマンドを区切るため、現在のディレクトリの内容が一覧表示されます。これは単純な例ですが、攻撃者はrm -rf /やcat /etc/shadowなど、さらに悪意あるコマンドを入力する可能性があります。

eval()の安全でない使用

Pythonのeval()関数は、文字列をPython式として動的に評価し、その結果を返します。強力な機能である一方、諸刃の剣でもあります。次のPythonコードを見てみましょう。

# Vulnerability: Insecure use of the eval() method.
user_input = input("Enter a Python expression to evaluate: ")
try:
    result = eval(user_input)
    print(f"Result:\n{result}")
except Exception as e:
    print(f"Error:\n{e}")

このコードでeval()を使うのは危険です。数学的な式や単純なPython式だけでなく、あらゆるPythonコードを評価するためです。さらに、osなどのモジュールを使って基盤となるシステムとやり取りできるPythonの機能と組み合わさると、リスクが高まります。

たとえば、攻撃者は次のような入力を行えます。

__import__('os').popen('ls').read()

上記の入力例はosモジュールをインポートし、Unix系システムでlsコマンドを実行してディレクトリの内容を一覧表示します。これは簡単な例ですが、悪意ある攻撃者はさらに危険なコマンドを入力し、ファイルの改ざんやデータの持ち出し、さらにはシステムへの不正アクセスを試みる可能性があります。

os.popen('ls').read()を使ったPython式を実行するdemo.pyのターミナル。ディレクトリの内容が表示されている。

コマンドインジェクションの脆弱性を軽減する

コマンドインジェクションがPythonアプリケーションに与える潜在的な悪影響を考えると、この問題に効果的に対処することが重要です。幸い、次のガイドラインを取り入れることで、ソフトウェア開発ライフサイクル(SDLC)の早い段階から多くのコマンドインジェクションの脆弱性を予防できます。

入力を適切に検証・サニタイズする

ユーザーや外部ソースからの入力を決して信用してはいけません。必ず想定されるパターンと照らし合わせて入力を検証し、サニタイズしてください。

import re

def sanitize_input(user_input):
    # A simple example: Allow only alphanumeric characters
    return re.sub(r'[^a-zA-Z0-9]', '', user_input)

上記の例では、入力から英数字以外の文字を取り除いているだけです。これにより攻撃者がコマンドを連結する可能性はなくなりますが、実際のアプリケーションでは、既知の応答のみを許可するなど、さらに厳格なサニタイズが必要になるでしょう。

パラメーター化クエリとプリペアドステートメントを使用する

パラメーター化クエリとは、まずコマンドとその構造を定義し、挿入するパラメーターを別途指定する方法です。これにより、コマンドとデータを文字列として連結することがなくなります。両者を別々に扱うことで、システムはFdataを実行コードではなく、データとしてのみ処理します。

プリペアドステートメントでは、この考え方をさらに進めます。まずコマンドの構造を準備し、プレースホルダーに入れるパラメーターだけを送信します。たとえば、Pythonのsubprocessモジュールでは、パラメーター化クエリと同様に、コマンドと引数を分離してコマンドを実行できます。

import subprocess

command = "ls"
directory = input("Which directory to list? ")
subprocess.run([command, directory])

shell=Trueは慎重に使用する

Pythonのsubprocessモジュールを使うときは、特にユーザー入力を扱う場合、shell=True引数を避けてください。shell=Trueを指定してコマンドを実行すると、渡された文字列はすべてシステムシェルによって解釈されます。この文字列にユーザー入力が含まれていたり、適切なサニタイズなしに複数のソースから組み立てられていたりすると、攻撃者が悪意あるコマンドを注入する可能性があります。

command = input("Enter the directory to list: ")
subprocess.run(f"ls {command}", shell=True)

これは、不正なシステムアクセスやデータ窃取、マルウェアのインストール、権限昇格、任意のコマンド実行を招く危険な状態です。shell=Trueの使用は避けてください。どうしても必要な場合は、先に入力をサニタイズしましょう。

ユーザー入力をosのメソッドに渡さない

ユーザー入力をos.spawnなどのosメソッドに直接渡すと、Pythonアプリケーションがコマンドインジェクションに対して脆弱になります。そのため、用途に応じた代替のPython APIがある場合は、それを使用してください。osメソッドが必要な場合は、ユーザー入力を渡さずに使うようにしましょう。ユーザー入力が不可欠であれば、すべての入力をサニタイズし、コンテキストに応じたエンコード技術を使用してください。

権限を制限し、強固なアクセス制御を実装する

アプリケーションは常に、必要最小限の権限で実行してください。攻撃者が脆弱性を悪用した場合でも、被害を抑えられます。また、chroot、コンテナ、ユーザー名前空間などのツールや手法を使って、アプリケーションを隔離することも検討してください。

Pythonのセキュアコーディング規約

以下では、実践することでPythonアプリケーションをコマンドインジェクションの脆弱性から保護できる、セキュアコーディング規約をいくつか紹介します。

入力を適切に検証・サニタイズする

入力の検証とサニタイズは、Pythonアプリケーションをコマンドインジェクションから守るうえで不可欠です。ユーザー入力を厳密に検証し、適切にサニタイズすることで、攻撃対象領域を効果的に縮小し、不正なコマンド実行を防ぎ、ソフトウェア全体のセキュリティを高められます。

次のコードを見てみましょう。

def get_username():
    username = input("Enter your username: ")
    if len(username) < 3 or not username.isalnum():
        raise ValueError("Invalid username!")
    return username

この関数は、処理の前に入力が指定された長さであり、英数字のみを含むことを確認します。実際のアプリケーションでは、この簡単な例よりも厳格な検証ルールが必要になるでしょう。

subprocessモジュールなどの関数を慎重に使う

subprocessモジュールを使うと、新しいプロセスを起動し、その入力、出力、エラーの各パイプに接続して、戻り値を取得できます。そのため、このモジュールを安全に使用することが重要です。

必要な場合を除き、subprocessモジュールでshell=Trueを使うのは避けてください。シェル内でコマンドを実行することになり、コマンドインジェクション攻撃につながる可能性があります。

# Avoid
subprocess.run("ls -l", shell=True)

# Instead Use
subprocess.run(["ls", "-l"])

ユーザー入力と外部コマンドを安全に扱う

ユーザー入力をコマンド文字列に直接連結したり、埋め込んだりしないでください。必ず引数リストを使用してください。

user_input = input("Enter a directory name: ")

# Avoid
subprocess.run(f"ls {user_input}", shell=True)

# Prefer
subprocess.run(["ls", user_input])

SASTとSCAツールでセキュリティの問題を検出・修正する

静的アプリケーションセキュリティテスト(SAST)とソフトウェア構成分析(SCA)のツールを使うと、開発中のコードとソフトウェアで使用しているオープンソースコンポーネントの両方を分析し、開発ライフサイクルの早い段階で脆弱性を特定できます。

Snykプラットフォームは、ハイブリッドAIアプローチにより、セキュリティ上の問題をできるだけ簡単に発見・修正できます。コード内の脆弱性を見つけて修正し、潜在的なセキュリティリスクに関するリアルタイムのフィードバックと修正アドバイスを提供します。SnykのようなSAST・SCAツール(永久無料プランあり)を活用すれば、コードベースのセキュリティ問題を継続的に監視し、本番環境に影響が及ぶ前に早期に修正できます。これにより、次のようなリスクの防止に役立ちます。

  • システムやデータを侵害する可能性のある悪意のあるパッケージへの曝露

  • 信頼されていたコンポーネントが侵害され、それに依存するすべてのソフトウェアに影響を及ぼすサプライチェーン攻撃

  • 未修正の脆弱性が含まれている可能性のある古い依存関係

SnykのようなSAST・SCAツールを開発プロセスに組み込むことで、機能するソフトウェアを構築するだけでなく、絶えず進化するサイバー脅威からソフトウェアを守ることができます。

安全なPython開発のためのベストプラクティス

このセクションでは、チーム全体で安全なPython開発を推進するために取り入れられる、基本的なコーディング習慣とワークフローを紹介します。

定期的にコードレビューとセキュリティ監査を実施する

入力検証、認証の仕組み、機密データの取り扱いを徹底的に確認する定期的なコードレビューは、開発ライフサイクルの早い段階で潜在的な脆弱性を特定するのに役立ちます。セキュリティを専門とするチームメンバーが参加することで、安全でないコーディング慣行、設定ミス、サニタイズされていない入力など、初期開発時には見逃される可能性のある問題を発見できます。

定期的なセキュリティ監査は、進化する脅威に対してコードベースの堅牢性を保つうえで欠かせません。OpenVAS、ZAP、Metasploitなどのツールを使って、プログラムの脆弱性をテストできます。

ソフトウェアとライブラリを最新の状態に保つ

古いソフトウェアやライブラリには、既知のセキュリティ脆弱性が含まれていることがよくあります。Pythonインタープリター、フレームワーク、サードパーティ製ライブラリを常に最新バージョンに更新しましょう。pipなどのパッケージマネージャーやrequirements.txtなどの設定ファイルを使うと、依存関係を効率的に管理・更新できます。

セキュリティツールとフレームワークを活用する

Python向けに設計されたセキュリティツールやフレームワークを活用し、脆弱性の検出を自動化できます。Snykなどのツールは、コードレベルの問題やセキュリティ設定の不備、サードパーティ製パッケージの脆弱性の特定に役立ちます。

Pythonの脆弱性を検出して修正

Snykの脆弱性スキャンと修正アドバイスで、アプリケーションを保護しましょう。

クレジットカードは不要です。

またはAzure AD、Docker ID、Bitbucketで登録

Snykを利用することで、利用規約およびプライバシーポリシーを含む各種ポリシーに同意したものとみなされます。

セキュリティを重視する意識を育む

開発者のセキュリティ意識を高める文化を育むことが重要です。セキュリティに関する研修やワークショップ、リソースを提供し、開発者の知識を深めましょう。開発者が最初からセキュリティを優先すれば、ソフトウェア全体の品質が向上し、脆弱性のリスクを軽減できます。

セキュリティを重視する企業文化の構築について学ぶには、Secure Developer Podcastがおすすめです。企業のセキュリティリーダーへのインタビューを多数収録し、各社がどのように開発プログラムを構築しているかを掘り下げています。

リアルタイムの脆弱性検出に役立つIDE拡張機能は、コードを書きながら脆弱性のあるコードを特定できる便利なツールです。開発環境に直接統合され、潜在的なセキュリティ問題をリアルタイムで表示します。

Visual Studio Codeに、サニタイズされていないユーザー入力によるコマンドインジェクションについてのSnykの警告とPythonコードが表示されています。

開発中に脆弱性を見つければ、開発サイクルの後半に修正する場合と比べて、修正にかかる時間と労力を節約できます。後になればなるほど、修正のコストは大幅に増加します。

まとめ

コマンドインジェクションは、多くのPythonアプリケーションに影響を及ぼす深刻な問題です。このガイドでは、この脆弱性がどのように発生するのか、またそれに伴うリスクについて解説しました。脆弱なコードパターンを見分けることで、潜在的な脅威と必要な対策について理解を深められます。

また、入力検証や、コマンド実行に使う組み込みのシステムモジュールを慎重に扱うことなど、安全なコーディングの慣習についても学びました。これらの戦略を実践することで、Pythonアプリケーションのセキュリティを強化し、攻撃を防ぎ、ソフトウェア全体のレジリエンス向上に貢献できます。

開発プロセスをさらに強化するために、Snykなどのツールの活用をご検討ください。SnykのIDE拡張機能はリアルタイムのセキュリティフィードバックを提供し、コードの記述中にスキャンして、すぐに適用できる修正を提案します。セキュリティの専門家がそばで確認してくれるように、最初から安全で堅牢なコードを書けるよう支援します。

Pythonアプリのセキュリティ対策を始めましょう

Snykを使って、Pythonの脆弱性を無料で検出・修正できます。

クレジットカードの登録は不要です。

またはAzure AD、Docker ID、Bitbucketで登録

Snykを利用すると、利用規約およびプライバシーポリシーを含む各種ポリシーに同意したものとみなされます。