PHP2Shell で騒ぐ前に知っておくべきこと:「PHPのバージョンを上げれば終わり」ができない現場の構造的欠陥

最近、「PHP2Shell」と呼ばれる深刻な脆弱性が話題になり、SNSやセキュリティ関連のニュースで大きく取り上げられています。

これに対して「何をそんなに大騒ぎしているのか? PHPのバージョンを安全な版にアップデートすれば終わる話ではないのか?」と感じた人も多いのではないでしょうか。

結論から言えば、その通りです。

技術的な解決策としては「修正されたバージョンに上げる」のが唯一かつ完全な正解です。しかし、それでもこれほどの大騒ぎになっているのは、「バージョンを上げることすらできないレガシーシステムが、世の中に大量に放置されているから」に他なりません。

この記事では、なぜ「バージョンアップ」という当たり前の対応ができないのか、そしてその裏にある現場の構造的な課題について整理します。

 

🤔 なぜ「PHPのバージョンアップ」で終わらないのか?

今回の脆弱性は、PHP 5.4から8.5という非常に広い範囲に影響を与えます。そして、ここが最大のネックなのですが、すでにサポートが終了(EOL)しているPHP 8.1以前のバージョンには、公式からのパッチが提供されません。

つまり、古いPHPを使っている環境は「修正版の8.2以降」へ一気にジャンプアップ(メジャー/マイナーバージョンアップ)する必要があります。

しかし、古いPHPで動いているシステムをそのまま最新のPHP 8.2/8.3に持っていくと、以下のような問題が確実に発生します。

・ 非推奨(Deprecated)機能・廃止関数の影響で画面が真っ白になる
・ 古いフレームワークやWordPressプラグインが動作しなくなる
・ データベースの接続ドライバや仕様変更に伴う挙動不審

結果として、「ただPHPのバイナリを差し替える」だけでは済まず、アプリケーションコードの大規模な改修と全機能の再テストが必要になります。

 

🤔 バージョンアップできないシステムは、ただの「延命」である

「コードを直せばいいじゃないか」と思うかもしれませんが、現場ではそれができません。

・ 仕様書がなく、当時の開発者もいない(誰も全体像を把握していない)
・「現状動いているから触るな」という経営陣・決裁者の圧力
・ 売上を生み出さない基盤改修に予算が出ない

このようにして、「修正費用が出せないから」「壊れるのが怖いから」とバージョンアップを先送りにしてきたシステムが、今回のPHP2Shellのような致命的な脆弱性に晒された瞬間、手も足も出なくなるのです。

WAF(Web Application Firewall)の設定調整や、一時的な設定変更で攻撃を防ごうとする現場もありますが、これはあくまで「時間稼ぎの応急処置」にすぎません。

バージョンアップに追従できないシステムは、ソフトウェアとしての寿命をすでに迎えており、「技術的負債の返済を先送りして延命しているだけ」というのが残酷な現実です。

 

🤔 開発者・エンジニアが取るべき姿勢

今回の騒動から私たちが学ぶべきなのは、「脆弱性が出たときにどう対処するか」だけではありません。「そもそもバージョンアップを前提としたアーキテクチャや保守体制をどう維持するか」という点です。

・ フレームワークや言語のマイナーアップデートを日々の運用に組み込む
・ EOL(サポート終了)を迎える前に、計画的なマイグレーション予算を確保する
・「動いているから触らない」ではなく、「触り続けられる状態(テストコードやCI/CD)を保つ」

これらができていないシステムは、どれだけ強固なセキュリティ製品を前段に置いたとしても、いつか必ず破綻します。

 

🤔 まとめ

「PHPのバージョンを上げたら終わりじゃないの?」という疑問に対する答えは、「理屈の上ではその通り。でも、上げられないシステムがあまりにも多すぎるから大騒ぎになっている」です。

セキュリティの脆弱性問題は、単なる技術の問題ではなく、これまでのシステム運用・投資判断のツケが可視化された結果とも言えます。

「そもそも、アナウンス済みのさまざまな脆弱性を放置したままだったよな?」

自社で管理しているシステムや関わっているプロダクトがあるなら、今回の件を機に「いつでも言語バージョンを上げられる状態になっているか?」を改めて見直してみるのが良いかもしれません。

👉 PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix two critical vulnerabilities that can allow unauthenticated remote code execution. All PHP users are strongly encouraged to update immediately.


Troubleshooting SQLDelight: The "KotlinSourceSet 'main' not found" Error in AGP 9

If you've recently tried jumping onto the bleeding edge with Android Gradle Plugin (AGP) 9.0, you might have hit a brick wall during project sync. A common culprit popping up lately is a cryptic error from SQLDelight:


KotlinSourceSet with name 'main' not found.

This issue, tracked under SQLDelight #6078, highlights a significant shift in how Android and Kotlin interact in the latest build tools. Here’s the breakdown of what's happening and how to fix it.

👉 KotlinSourceSet with name 'main' not found with AGP9 · Issue #6078 · sqldelight/sqldelight

 

🧑🏻‍💻 The Problem: Why is 'main' Missing?

The conflict arises because AGP 9 introduces a "New DSL" and changes how Kotlin source sets are managed. Historically, SQLDelight’s Gradle plugin looked for a source set explicitly named "main" to inject its generated code.

However, in AGP 9 (especially with the builtInKotlin flag enabled), the way source sets are registered has changed. The legacy "main" container that SQLDelight expects is either missing or hidden, causing the configuration phase to fail immediately.

 

🧑🏻‍💻 The Discussion Flow: From Discovery to Workaround

The GitHub thread reveals an interesting evolution of the fix:

  • The Discovery: Early adopters reported that even disabling the experimental newDsl didn't fix the crash.
  • The Culprit: Contributors identified that the SQLDelight plugin was making "unsafe" assumptions about the existence of the "main" source set.
  • The Temporary Fix: A specific flag in gradle.properties was found to restore the old behavior, allowing the plugin to find what it needs.

 

🧑🏻‍💻 How to Fix It (The Workaround)

Until the official patches in SQLDelight (specifically PR #6079 and #6091) are fully merged and released, you can unblock your development by adding these flags to your gradle.properties file:


# Temporary fix for SQLDelight + AGP 9
android.newDsl=false
android.builtInKotlin=true
android.disallowKotlinSourceSets=false

The key line here is android.disallowKotlinSourceSets=false. This tells AGP 9 to allow the traditional Kotlin source set structures that SQLDelight currently relies on.

 

🧑🏻‍💻 The Road Ahead: Permanent Fixes

The maintainers (including Jake Wharton and the CashApp team) are already working on a long-term solution. The goal is to move away from searching for "main" and instead use the proper AGP/Kotlin API to register generated code.

  • PR #6079: Focuses on implementing a more robust schema configuration.
  • PR #6091: Modernizes the plugin to play nice with the AGP 9 New DSL.

The takeaway?

AGP 9 is a major shift. If you’re using third-party plugins that generate code (like SQLDelight or Wire), expect a few bumps in the road as these libraries catch up to the new Gradle architecture.


Activity x Compose Lifecycle: The Complete Visual Guide 🚀

 

👨‍💻 Activity Lifecycle


Activity.onCreate() <- Activity is created

↓

Activity.onStart() <- Preparing to become visible on screen

↓

Activity.onResume() <- Foreground state (interactive)

↓

Activity.onPause() <- Partially visible

↓

Activity.onStop() <- No longer visible

↓

Activity.onDestroy() <- Activity is destroyed

 

👨‍💻 Compose Lifecycle


[First Composition]
  - Composable is evaluated, Compose tree is built
  - LaunchedEffect -> Runs once after the commit
  - SideEffect -> Runs after every commit
  - DisposableEffect -> onDispose is called upon disposal

↓

[Recomposition]
  - UI is re-evaluated in response to state or data changes
  - Only the necessary parts are recomposed (efficient update)
  - SideEffect and DisposableEffect are also re-evaluated during recomposition

↓

[Dispose]
  - Depends on the ComposeView's disposal condition
  - DisposableEffect's onDispose is executed
  - Disposal timing is determined by the ViewCompositionStrategy

 

👨‍💻 Activity x Compose Lifecycle


Activity.onCreate() setContent { ... } <- Sets the ComposeView

↓

[First Composition]
  - Evaluates Composables and builds the UI
  - LaunchedEffect -> Runs once after commit
  - SideEffect -> Runs after each commit
  - DisposableEffect -> Defines onDispose

↓

Activity.onStart()

↓

Activity.onResume()

↓

[Recomposition]
  - Re-evaluates necessary parts in response to state changes
  - LaunchedEffect is not re-executed (only if its key changes)
  - SideEffect / DisposableEffect are re-evaluated

↓

Activity.onPause()

↓

(ComposeView is retained)
  - UI becomes partially obscured
  - State is maintained within the Composition

↓

Activity.onStop()

↓

[Preparing for Dispose (Detection)]
  - ViewCompositionStrategy monitors disposal conditions

↓

Activity.onDestroy()

↓

[Dispose]
  - ComposeView is destroyed
  - DisposableEffect.onDispose() is executed

This is the general flow.

Here are the notes for each item:

Activity.onCreate() A lifecycle method called when an Android app's Activity is created.

    setContent { ... }: Sets the UI using Jetpack Compose. This sets a ComposeView as the Activity's content view. The Compose lifecycle begins.

  • First Composition The Composable functions set in setContent are evaluated for the first time, and the UI is built.

LaunchedEffect: An Effect used for asynchronous processing. Runs only once after the first composition.

SideEffect: Runs after every composition commit (the timing when UI changes are applied).

DisposableEffect: An Effect used for resource cleanup. The logic defined in onDispose is executed when the Composition is disposed.

Activity.onStart() A lifecycle method called just before the Activity becomes visible to the user.

Activity.onResume() The Activity moves to the foreground and becomes fully interactive with the user.

  • Recomposition Only the necessary Composable functions are re-evaluated in response to changes in State.

LaunchedEffect: It is not re-executed during recomposition. However, it will be re-executed if its key changes.

SideEffect: It is re-evaluated on every recomposition.

DisposableEffect: It is re-evaluated on recomposition, and the cleanup logic (onDispose) from the old Effect is called.

Activity.onPause() The Activity enters a paused state and becomes partially obscured.

ComposeView Retention: The UI is not destroyed; it is retained. The state is also maintained within the Composition, allowing it to be reused upon re-display.

Activity.onStop() The Activity is no longer visible. A time to prepare for cleaning up state and resources.

Preparing for Dispose (Detection) ViewCompositionStrategy: A mechanism that monitors the conditions under which the ComposeView should be disposed. It triggers the Composition's disposal based on the View's lifecycle.

Activity.onDestroy() The timing when the Activity is completely destroyed.

  • Dispose The ComposeView is destroyed: The UI and state are completely released.
  • DisposableEffect.onDispose(): The resource cleanup logic is called.

 

👨‍💻 Summary

The Jetpack Compose and Activity lifecycles are closely linked, with specific processes occurring at each stage, from UI initialization (setContent) to disposal (Dispose).

It is especially important to understand the differences between effects like LaunchedEffect, SideEffect, and DisposableEffect, and to manage them appropriately.

Furthermore, by efficiently reusing the UI in response to Activity state changes (like onResume or onPause) and cleaning up resources as needed, you can achieve stable application behavior.


初回コンポーズは Activity.onStart() までに終わる

 

🤔 Activity x Compose のライフサイクル


Activity.onCreate()
  setContent { ... }   ← ComposeView をセット

    ↓

  [初回コンポーズ(First Composition)]
    ・Composable を評価し UI を構築
    ・LaunchedEffect → コミット後に一度実行
    ・SideEffect → 各コミットごとに実行
    ・DisposableEffect → onDispose 定義

    ↓

Activity.onStart()

    ↓

Activity.onResume()

    ↓

  [再コンポーズ(Recomposition)]  
    ・状態変更に応じて必要部分のみ再評価
    ・LaunchedEffect は再実行されない(Key 変更時のみ)
    ・SideEffect / DisposableEffect は再評価

    ↓

Activity.onPause()

    ↓

  (ComposeView は保持)
    ・UI は部分的に見えなくなる
    ・状態は Composition 内で維持

    ↓

Activity.onStop() 

    ↓

  [破棄準備(Dispose 検知)]
    ・ViewCompositionStrategy による破棄条件監視

    ↓

Activity.onDestroy()

    ↓

  [破棄(Dispose)]
    ・ComposeView が破棄される
    ・DisposableEffect.onDispose() 実行

 

🤔 まとめ

初回コンポーズは Activity.onStart() までに終わる。

👉 これだけでわかる!Activity × Compose のライフサイクル完全図解 🚀


JSON と JSON5 の違い

JSON と JSON5 は見た目は似ていますが、「どれくらい人間に優しいか」「どれくらい厳格か」で大きく違います

 

🧩 JSONとは

JavaScript Object Notation の略で、
データ交換フォーマットとして最も一般的なものです。

 

✅ 特徴

・厳格でシンプル
・仕様が固定されていて機械処理に最適
・ほとんどの言語・ツールが標準対応

 

📋 制約


{
  "name": "Mao",
  "age": 25,
  "languages": ["Swift", "Kotlin"]
}

・文字列は必ず ダブルクォート
・コメント不可
・末尾カンマ禁止
・キーもクォート必須
・数値表現 は10進数のみ

 

🌈 JSON5とは

JSON for Humans(人間に優しいJSON)で
JSONの上位互換として提案されたフォーマットです。

JavaScriptライクな書き方を許します。

 

✅ 特徴

・より柔軟で可読性が高い
・コメントや末尾カンマをサポート
・キーや文字列のクォート省略OK
・16進数や+∞などの数値表現が可能

 

📋 例


{
  // コメントOK
  name: 'Mao',
  age: 25,
  languages: ['Swift', 'Kotlin',], // 末尾カンマOK
  version: 0x1f, // 16進数OK
}

 

🤔 JSON と JSON5 の比較

JSON は機械向けの厳格フォーマット、JSON5 は「人間向け」に拡張されたフォーマットです。下の表はブログでそのまま使える比較表です。

項目 JSON JSON5
コメント 不可(コメントなし) 可(`//`、`/* */` が使える)
末尾カンマ 不可 可
キーのクォート 必須(ダブルクォート) 省略可(識別子としてのキーを許可)
文字列のクォート ダブルクォートのみ(`"`) シングルクォートまたはダブルクォート可(`'` / `"`)
数値表現 10進数のみ 10進数、16進数、`NaN`、`Infinity` などを許可
対応ツール 非常に多い(ほぼ標準) ライブラリが必要(サポートは限定的)
用途 機械間通信(API等)に最適 開発者向け設定ファイルや手で編集するデータに適する

 

🧑🏻‍💻 まとめ

- JSON:厳格で機械向け。安全・標準的。
- JSON5:柔軟で人間向け。コメントや末尾カンマOK。