0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ChatGPT Linux版の.desktopをPythonで監査する

0
Posted at

デスクトップアプリなら、ブラウザで使うより単純。そう思って入れた直後に見るべきなのは、アプリの画面ではなく .desktop ファイルです。

8月11日にChatGPTのLinux向けデスクトップアプリが発表された。こういうクライアントが増えると、アプリ本体だけでなく、メニューから何を起動するかを決める定義も手元に増える。Linuxではその役を .desktop が持つ。

今日はテスト用の定義を2つ作って、ExecTerminal をPythonで見比べた。目で開けば一瞬なのに、更新後の差分まで毎回読むのは続かないんだよね。雑でも同じ基準で警告を出す方が、まずは役に立つ。

何を確認すればいい?

最初に見るのは3点です。

  • Exec: どの実行ファイルに、どんな引数を渡すか
  • Terminal: 起動時に端末を開く指定があるか
  • MimeType: 独自URLスキームを受け取る登録があるか

ユーザー単位の定義は ~/.local/share/applications、システム全体の定義は /usr/share/applications に置かれることが多いです。まず一覧だけ取るならこれで足ります。

find ~/.local/share/applications /usr/share/applications \
  -maxdepth 1 -name '*.desktop' 2>/dev/null | sort

ここで意識したいのは、Exec はシェルスクリプトではない点です。|; が見えたからといって、それだけでシェルの演算子になるわけではありません。逆に /bin/sh -c のようにシェルを明示していたら、そこで初めて引数全体を別の目で読む必要が出ます。

この3点は、次の順番で確認すると漏れにくい。

まずは起動定義だけを機械的に見る

次のスクリプトは、完全な仕様検証器ではありません。新しく入れたクライアントや更新差分を読む前の、粗い足切りです。絶対パスで直接バイナリを呼んでいるか、シェルを挟んでいないか、端末起動を要求していないかを調べます。

from __future__ import annotations

import configparser
import os
import shlex
import sys
from pathlib import Path

SHELLS = {"sh", "bash", "zsh", "fish", "dash", "ksh"}

def audit(path: Path) -> list[str]:
    parser = configparser.ConfigParser(interpolation=None, strict=False)
    parser.optionxform = str
    parser.read(path, encoding="utf-8")

    if "Desktop Entry" not in parser:
        return ["Desktop Entry セクションがない"]

    entry = parser["Desktop Entry"]
    if entry.get("Type") != "Application":
        return [f"Type が Application ではない: {entry.get('Type', '(未指定)')}"]

    exec_line = entry.get("Exec", "")
    if not exec_line:
        return ["Exec が空"]

    try:
        argv = shlex.split(exec_line)
    except ValueError as exc:
        return [f"Exec を分解できない: {exc}"]

    if not argv:
        return ["Exec が空"]

    findings: list[str] = []
    command = Path(argv[0])
    if command.name in SHELLS:
        findings.append(f"Exec がシェルを起動する: {argv[0]}")
    if not command.is_absolute():
        findings.append(f"実行ファイルが絶対パスではない: {argv[0]}")
    elif not command.is_file() or not os.access(command, os.X_OK):
        findings.append(f"実行できるファイルが見つからない: {command}")
    if entry.get("Terminal", "false").lower() == "true":
        findings.append("Terminal=true になっている")

    return findings

if __name__ == "__main__":
    for arg in sys.argv[1:]:
        target = Path(arg)
        findings = audit(target)
        if findings:
            print(f"[WARN] {target}")
            for finding in findings:
                print(f"  - {finding}")
        else:
            print(f"[OK] {target}: Exec は直接実行、Terminal=false")

/usr/bin/printf %U/bin/sh -c '/tmp/update-client' を入れた2つのテスト用定義で実行しました。前者は通り、後者だけが警告になります。

[OK] /dev/fd/11: Exec は直接実行、Terminal=false
[WARN] /dev/fd/12
  - Exec がシェルを起動する: /bin/sh
  - Terminal=true になっている

警告が出たら、どこまで掘る?

この出力だけで危険と決める必要はありません。たとえばElectron系のアプリにはラッパーが入り、絶対パスでない Exec が正当な場合もあります。ただ、sh -cTerminal=true、実体のない絶対パスが同時に出たら、そのまま起動せず配布元のパッケージ内容と更新履歴を確認します。

URLスキームも同じです。MimeType=x-scheme-handler/... があること自体は普通ですが、ブラウザや別アプリから渡されたURLで起動する入口になります。Exec の引数に %u%U があるなら、そのURLをアプリがどう扱うかまで一度追いたいところです。ここは文字列検査だけでは分かりません。

おわりに

Linuxアプリの導入確認は、署名やハッシュだけでは終わりません。起動経路を一度読むと、更新で何が変わったかも追いやすくなります。

このスクリプトは警告を減らすためのものではなく、読むべきファイルを絞るためのものです。新しいAIクライアントほど、まず .desktopExec を差分で見る。この小さい習慣を入れておくと、アプリの便利さと起動時の挙動を分けて判断できます。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?