月: 2026年9月 Page 1 of 2

ワイヤレスイヤホン・新調。(Anker Liberty Pro4選定)

2024年以来のワイヤレスイヤホン、導入です。

Bluetoothイヤホンを買い換えました。筆者が最終的に選択したのはAnkerのSoundcore Liberty 4 Proです。

それまで使用していたモデルは、同社のSport X20。イヤーフックがあるため落としにくいという利点がありましたが、アクティブノイズキャンセリングの効き具合に関してはやや物足りなくなっていったというのも事実です。特に移動時や周囲の雑音が気になる環境では、より強力な遮音性能がほしいというのも購入のきっかけである。

選択肢Pro 4 かPro 5か

SportはX20が未だ現役ということから、イヤーフックなしでも使える機種を選定していきます。選択肢として検討したのは、

  • 昨年登場したLiberty Pro 4
  • 今年新たに発表された後継機のLibetty Pro 5です。

最新モデルであるPro 5は性能面を見れば非常に魅力的ではありましたが、ここに一つの問題が立ちはだかります。

落としても授業料で済むか?

筆者は過去に、当時のSONYのハイエンドモデルを側溝に落として紛失してしまった経験があります。いくら2年使っていたとはいえ、高価格なイヤホンを失った際の精神的・金銭的ダメージは小さくなく、だからこそこれまである程度割り切って使える価格帯(そして落ちにくい形状)の製品を選んできました。

なので、Pro 5の約2.5万円強という価格設定は、万が一の紛失を想像した際にやや心理的ハードルが高いというものがあります。

一方でPro 4は、実売1万円台半ばから後半という価格帯でありながら、十分強力なノイズキャンセリング性能を確認。万が一の紛失時のリスクを許容範囲に抑えつつ、日常の静寂を手に入れるという点も協力でした。

確定、Pro 4

また、Pro 4のスマート充電ケースにディスプレイが搭載されている点もポイントでした。ケース側でノイズキャンセリングの切り替えやモード設定を直接行えるため、歩行中などに耳元のイヤホン本体を直接触る機会が減る。これは耳元での操作ミスによるポロリと落とす事故をある程度防げます。

それ以上に、実機を装着して「自分を選んだ」感が強いのものPro 4でした。

使用感、気になったことなどは改めてご報告します。

プライズ:ライザのアトリエ3版フィギュア撮影。

今回は撮影に相当苦労しました。

BiCute Bunnies ライザのアトリエ3版。

  • 色飛びしやすい白
  • 強烈なバニースーツの質感
  • しっかり本物が使われているフィッシュネットパンスト。

これらが重なり、撮影は過去最大レベルでの難易度でしたが

S字カーブやパンストの質感

脇からうなじにかけての質感も非常に良くできていて、スケールフィギュア以上だと思った次第です。

Apache RewriteRuleを書き直して気付いたこと。「正規表現」は短く書くためではなく、保守しやすくするために使う

先日、Redmine の Googlebot 対策として Apache の mod_rewrite を使った RewriteRule を追加しました。

まずは CPU 使用率を下げることが最優先だったため、とにかく確実に動くことを重視して場当たり的に正規表現を追加しました。

なので、今回は、RewriteRule を書き直しながら改めて感じた、「正規表現は短く書くためではなく、保守しやすくするための道具」という話です。

動けばいい。だが、そんな状態では後でメンテしづらい

最初に書いた設定はこんな感じでした。

RewriteRule ^/projects/.*/knowledgebase - [G,L]
RewriteRule ^.*/knowledgebase - [G,L]

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.pdf$ - [G,L]

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.csv$ - [G,L]

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.atom$ - [G,L]

もちろん、これでも動きます。むしろトラブル対応中であれば、このくらい分かりやすく書いた方が安全です。

ただ、冷静になって眺めてみると、

「同じことを何回も書いている」

ことに気付きました。

正規表現とは何か

そこで、さらに正規表現で直していきます。「正規表現」という言葉を聞くと、難しそうな印象を持たれる方も多いかもしれませんが、思ったよりも単純なルールでできます。

例えば、

issues.pdf
issues.csv
issues.atom

この3つは末尾だけが違います。

これを

issues\.(pdf|csv|atom)

と書けば、

.pdf でも .csv でも .atom でも一致する」

という意味になります。MtGで言うなれば、

  • 日没を遅らせる者
  • 時を解す者
  • ドミナリアの英雄

につく「テフェリー」は

(日没を遅らせる者|時を解す者|ドミナリアの英雄)、テフェリー

と書けます。このように、共通点があるものを一つのルールで表現する。それが正規表現です。

書き直した結果

最終的には次のような形に整理しました。

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteCond %{REQUEST_URI} ^/(projects/[^/]+/)?issues\.(pdf|csv|atom)$ [NC,OR]
RewriteCond %{QUERY_STRING} (^|&)(sort|set_filter|per_page)= [NC]
RewriteRule ^/(projects/[^/]+/)?issues - [G,L]

PDF、CSV、Atom を一つの正規表現へまとめ、さらに「動的クエリ」と「エクスポート要求」を一つの RewriteRule で処理できるようにしています。

行数だけを見ると少し減った程度です。

ですが、保守性はかなり向上しました。

保守性を求めた正規表現

今回の目的は「短く書くこと」ではありません。

例えば将来、

issues.json

も遮断対象にしたくなったとします。以前の書き方なら RewriteRule を一本追加します。この書き方であれば

(pdf|csv|atom|json)

と一か所を書き換えるだけで済みます。変更箇所が一つだけになる。これは保守する上でかなり大きな違いです。

Knowledgebaseも同じ考え方で整理。

Knowledgebase の RewriteRule も整理しました。

以前は

RewriteRule ^/projects/.*/knowledgebase - [G,L]
RewriteRule ^.*/knowledgebase - [G,L]

のように複数行で書いていました。これを

RewriteRule ^/(home/www-data/|(.*/)?knowledgebase) - [G,L]

という一つのルールへまとめています。さらに、このルールでは以前問題になった

/home/www-data/...

のような物理パスへの誤アクセスも同時に処理しています。

「Knowledgebaseだけ」ではなく、「Rails に渡したくないもの」という視点で整理し直した結果です。

「同じ意味のもの」をまとめる

今回 RewriteRule を整理していて感じたのは、正規表現を書くことが目的ではないということです。目的は、「同じ意味を持つものを一つのルールで表現する」ことでした。

  • Knowledgebase は「存在しない旧機能」です。
  • PDF、CSV、Atom は「重いエクスポート」です。
  • sort や set_filter は「重い動的クエリ」です。

そう考えると、設定ファイルも「何を止めたいのか」が分かる構成になっていきます。

未来の自分が読める設定にする

サーバー設定は、一度書いたら終わりではありません。半年後、一年後あるいは障害対応中の深夜に、また自分が読むことになります。

その時に

「この RewriteRule は何を止めているんだっけ?」

と悩むようでは、あまり良い設定とは言えません。もちろん、正規表現は凝ろうと思えばいくらでも複雑にできます。ですが、複雑さは必ずしも保守性につながりません。

今回の書き直しで目指したのは、「短い設定」ではなく、 「意図がまとまっていて、後から見ても理解しやすい設定」 でした。
正規表現は、そのための道具なのだと改めて感じた次第です。

Redmine 6.x移行記(後編)犯人はGooglebotだった。Apacheの410 GoneでPassengerを守るまで

前回の記事では、Redmine 6.1への移行後に Passenger の CPU 使用率が異常に高止まりし、production.log と netstat を追い始めたところまでを書きました。

調べていく中で、

  • 削除した Knowledgebase へのアクセス
  • sort=set_filter= を含むチケット一覧
  • PDF や CSV、Atom のエクスポート要求

といった、一見すると関連性のないリクエストが大量に飛んできていることが分かってきます。そして最後に、

「もしかしてクローラーなのでは?」

という疑問が残りました。今回は、その続きを書いていこうと思います。

アクセス元を確認してみる

まずは production.log とアクセスログを突き合わせながら、アクセス元を確認してみました。

すると、見えてきたのは見慣れた IP アドレスです。

66.249.xx.xx

Googlebot でした。しかも一度や二度ではありません。HTTPS のセッションを何十本も張りながら、次々とリクエストを投げています。

しかもアクセスされていたのは、削除した Knowledgebase だけではありません。チケット一覧についても、

sort=
page=
set_filter=

といったパラメータ違いを延々と巡回しています。さらに、

issues.pdf
issues.csv
issues.atom

まで取得しようとしていました。普段ブラウザで Redmine を使っていると、これらは「便利な機能」という認識です。

ですが、サーバー側から見ると話は変わります。例えば PDF の生成であれば、データベースからチケットを取得し、テンプレートを組み立て、PDF をレンダリングし、レスポンスを返す。

HTML を返すだけとは比較にならないくらい重い処理になります。

つまり Googlebot は、リンクがあるから辿っているだけなのですが、Passenger からすると重い仕事ばかり持ち込まれている状態でした。

404でもPassengerは起きてしまう

Knowledgebase のアクセスについても同じです。

「存在しないページなんだから404で終わりでは?」

最初はそう考えていました。しかし実際には、Rails が起動し、ルーティングを行い、存在しないことを確認し、404 を返します。404 を返すこと自体は正しい動作です。ですが、その404を返すためにも Passenger は仕事をしています。

これでは、存在しないページへのアクセスであっても CPU を使い続けることになります。

ならばRailsまで届かせなければいい

ここで考え方を変えました。Rails が重いのではありません。Rails に仕事を渡してしまっていることが問題です。

であれば、Rails が起きる前にApacheで止めればいい。Apache を使っている以上、一番軽いのは Apache の段階で処理してしまうことです。

そこで mod_rewrite を使うことにしました。バーチャルホストの.confを以下のように変えていきます。

RewriteRule ^/projects/.*/knowledgebase - [G,L]
RewriteRule ^.*/knowledgebase - [G,L]

まずは削除済みの Knowledgebase。続いて、

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.(pdf|csv|atom)$ - [G,L]

PDF、CSV、Atom のエクスポート。さらに、

RewriteCond %{QUERY_STRING} (sort=|set_filter=|per_page=) [NC]
RewriteRule ^/(projects/[^/]+/)?issues - [G,L]

検索エンジンが総当たりしている動的クエリも Apache 側で受け止めることにしました。

404ではなく410を選んだ理由

404でも目的は達成できます。ですが、今回は410 Goneを選びました。

404エラーは、「今は見つからない」という意味です。検索エンジンから見ると、

「また後で来れば復活しているかもしれない」

という扱いになります。一方、410 Gone は、

「もう永久にありません。」

という宣言です。今回削除した Knowledgebase は、将来復活させる予定はありません。だったら、その意思を HTTP ステータスとして返した方が正確です。

robots.txtも追加した

もちろん、Apache の設定だけではありません。今後の巡回そのものを減らすため、robots.txt にも、

Disallow: /projects/*/knowledgebase*
Disallow: /*?*sort=
Disallow: /*?*set_filter=

などを追加しました。ただし、ここは誤解しやすいところです。robots.txt は即効性のある仕組みではありません。

既に巡回予定へ入っているリクエストは、そのまま飛んできます。まず Apache で止める。その上で robots.txt により今後の巡回を減らす。この二段構えにしました。

効果はすぐに現れた

設定を反映し、Passenger(mod_passengerなのでapacheそのもの)を再起動します。

その後、再び CPU 使用率を確認すると、先ほどまで70〜90%を推移していた Passenger が数%程度で落ち着いていました。

production.log からも

  • Knowledgebase の404、
  • IssuesController の大量アクセス、
  • 500.html

の RoutingError は姿を消しています。試しに curl から Googlebot の User-Agent を付けてアクセスしてもApache が Rails を起動することなく、即座に

HTTP/1.1 410 Gone

を返しました。狙い通りです。Passengerに到達すること無くは何も仕事をしていません。

今回学んだこと

今回改めて感じたのは、「プラグインを削除すること」と、「削除したことを検索エンジンへ伝えること」は全く別の話だということでした。

Redmine 側では綺麗にプラグインをアンインストールできていても、Google は昔の URL を覚えています。そして真面目に巡回を続けます。そのリクエストを Rails まで届けてしまえば、存在しないページであっても Passenger は起き、CPU を使います。

だからこそ、410エラーにより「もうありません。」という判断を Apache が先に行う。

今回のケースでは、この水際防衛が一番効果的でした。

Redmine に限らず、長く運用してきた Web アプリケーションでは、大きな整理やプラグインの削除を行った後、一度アクセスログを眺めてみることをおすすめします。

自分ではとっくに忘れていた URL を、検索エンジンは驚くほど律儀に覚えているものです。そして、それが思わぬサーバー負荷につながっていることも、決して珍しくありません。

余談「Apacheは実は柔軟」

これは私の持論なのですが、超大手プラットフォームや大手配信サイトではapacheは重い。nginxこそ主流だ的な記事を見かけますが筆者のように

多数のサーバを立てられない
それでもWebアプリを多数動かしたい
セキュリティも譲れない
場合はApacheの方が驚くほど柔軟なパターンがあります。その辺の話はまた改めて。

Redmine 6.x移行後の運用記(前編) Passengerが静かにならない。Redmine 6.1移行後に始まった高負荷の正体を追う

Redmine 6.1への移行は無事に終わりました。

Redmine 5.1からのメジャーアップグレードということもあり、今回は不要になったプラグインをかなり整理しています。長年使い続けてきた環境だっただけに、「使っていないものは思い切って切る」という判断をしました。

しかし、Webで公開していると思わぬトラブルが発生しました。

CPUが一向に下がらない

切り替えた Redmine 6.1 を監視していると、Passenger の CPU 使用率が妙な動きをしています。

%CPU   %MEM   PID      USER     COMMAND
--------------------------------------------------------------------------------------
78.1   4.7    560493   www-data Passenger RubyApp: /home/www-data/redmine_v6 (prod)
38.4   3.8    560422   www-data Passenger AppPreloader: /home/www-data/redmine_v6

何も触っていないのに 70~90% を行ったり来たりしています。移行直後なので、

  • 「まだ何か設定を忘れているのか?」
  • 「プラグインを外した影響が残っているのか?」

と考えながら調査を始めました。

まず Passenger を見ようとした

最初に確認したのは Passenger 自身です。普段であれば passenger-status を実行すれば状況はある程度分かります。

ところが今回は、それすら使えませんでした。UNIX ドメインソケットのパス長制限(108バイト)に引っ掛かり、ArgumentError が発生してしまいます。

管理コマンドが使えない以上、地道にログを追うしかありません。そこで production.log と netstat を確認してみることにしました。

最初に見えたもの

production.log を眺めていると、最初に目に入ったのは見覚えのある URL でした。

Knowledgebase。

v5.1→v6.1へ移行で完全に削除したプラグインです。ところが、その URL へ今でもアクセスが来ています。

「検索エンジンが昔の URL を覚えているのか。」

最初はそう考えました。しかし、ログを読み進めていくと、それだけでは説明が付きません。

他にも出てくる動き

アクセスされているのは Knowledgebase だけではありません。

チケット一覧。

しかも普通に開いているわけではなく、

  • sort=
  • page=
  • set_filter=

こういったパラメータ付きの URL が大量に飛んできます。

さらに、

  • PDF
  • CSV
  • Atom

といったエクスポートまで巡回しています。

ここまで来ると、「404 が増えている」という話ではなくなってきました。クローラーがかなり重い処理を次々と要求しています。その頃、netstat を見ると HTTPS セッションが大量に張り付いたままになっていました。

ここでようやく、一つの可能性が頭に浮かびます。

「もしかして、これ全部クローラーなのでは……?」
「というか、悪質クローラーは止めている(includeでシャットダウンしている)はずでは……?」

そして、もしそうだとしたら、Rails 側で受け止め続けていいのか。

次回はログをさらに追い掛けながら、最終的に Googlebot による旧 URL の巡回と、動的クエリの総当たりが Passenger を疲弊させていた こと、そして Apache の mod_rewrite410 Gone を使って水際で止めるまでの経緯を書いていこうと思います。

刺身とフライ。

いつも訪れる定食屋にて。

鰹の刺身定食、そして

あじフライを注文。

たっぷり脂の乗った戻り鰹。ここは丁寧に生姜とネギが別添えなので味変にリズムつけられます。

ここで揚げ物を食べないのはこの店への敬意が欠けるとしてサクサクのアジフライ。ドラムスティックのように尻尾を持って食べられるのが最高です。

ですが、真に驚くべきは、最後に口を支配するのがご飯の甘みと香り。刺身もフライも味噌汁•漬物すら上書きする主食の破壊力はまさに定食屋の面目躍如でした。

【WAF検知ログ解析】攻撃者が探しているのは wp-config.php だけではない。そのバックアップでもある。(not only A but also B)

はじめに

以前、.env ファイルを狙った大量スキャンについて記事を書きました。

そのとき紹介したのは、「設定ファイルが公開されていないか」を世界中で機械的に探し回るボットの話です。

今回観測したログは、その続編とも言えるものでした。

狙われていたのは wp-config.php ……ではありません。

/wp-config.php.bak
/wp-config.php.old

つまり、人間が「作業時に残しておいたバックアップ」です。

今回もホスト名・IPアドレス・ディレクトリ名などは無害化しています。例によって、テロリストに名前を与えていません。

観測されたログ(マスキング済み)

[Fri Sep 04 00:55:48 2026] ModSecurity:
Rule 920440
URL file extension is restricted by policy
URI: /wp-config.php.bak

[Fri Sep 04 00:55:48 2026] ModSecurity:
Rule 930130
Restricted File Access Attempt
URI: /wp-config.php.bak

[Fri Sep 04 00:55:48 2026] ModSecurity:
Rule 920440
URL file extension is restricted by policy
URI: /wp-config.php.old

[Fri Sep 04 00:55:48 2026] ModSecurity:
Rule 930130
Restricted File Access Attempt
URI: /wp-config.php.old

数百ミリ秒という短時間で、.bak.old の両方を試しています。ブラウザで人間が操作しているとは考えにくく、自動化されたスキャナーによる探索と見てよいでしょう。

なぜ .bak.old を探すのか?

攻撃者が欲しいのは、バックアップファイルそのものではありません。その中に書かれている情報と「運用者の癖」です。

wp-config.php には、

  • データベースの接続情報
  • WordPressの認証キー(AUTH_KEY や SECURE_AUTH_KEY)
  • Cookieの暗号化キー
  • テーブルプレフィックス
  • サイト固有の設定

などが保存されています。本来の wp-config.php はPHPとして実行されるため、その内容をブラウザから読むことはできません。

しかし、

  • wp-config.php.bak
  • wp-config.php.old

になると話は別です。WebサーバーはこれらをPHPとして扱わず、単なるテキストファイルとして返してしまう場合があります。

このテキストファイルが表示されてしまえば、攻撃者はWordPressの脆弱性を探す必要すらありません。データベースへ直接接続するための情報が、そのまま手に入ってしまうからです。

玄関の鍵を壊そうとしているのではありません。玄関脇の植木鉢に、合鍵が入っていないかを確認している状態です。

本当に見られているのは「運用」

ここで興味深いのは、攻撃者が .bak を探していることではありません。

「そのようなファイルを公開領域へ置く運用をしているか」

を見ています。例えば

cp wp-config.php wp-config.php.bak
  • 編集前の退避。
  • FTPソフトが自動生成したバックアップ。
  • エディタが保存した一時ファイル。

どれも作業中であれば珍しいものではありません。問題なのは、それを公開ディレクトリへ置いたままにしてしまうことです。

もし .bak が見つかったら、多くのスキャナーはそこで終わりません。

続けて

  • wp-config.php.save
  • wp-config.php.tmp
  • wp-config.php.orig
  • backup/
  • old/
  • test/

などを次々に探し始めます。理由は単純です。

「バックアップを公開ディレクトリへ置く運用をしているなら、他にも何か残っている可能性が高い。」

と判断するからです。つまり、.bak はゴールではありません。攻撃者にとっては

「このサーバーは、まだ掘る価値がある」

という判断材料です。

ModSecurityは何を見ていたのか

今回ヒットしたのは二つのルールでした。

Rule 920440

まず反応したのは

URL file extension is restricted by policy

というルールです。.bak.old といった、本来ブラウザから取得されるべきではない拡張子を検知しています。

つまり、「その拡張子をHTTPで取りに来る時点で怪しい」という考え方です。

Rule 930130

続いて

Restricted File Access Attempt

が反応しています。こちらはファイル名そのものを見ています。

  • 危険な拡張子
  • 危険なファイル名

という二方向から同じリクエストを評価していました。一つのルールだけではなく、多層で検査するOWASP CRSらしい構成です。

ボットは「運用者の癖」を学習している

以前の記事では、

「多くの攻撃は、意志を持たず、目標を自動的に追尾するボットが大半を占めています。」

と書きました。今回のログも、まさにその延長線上にあります。ボットは感情を持って

「この管理者は雑そうだ」

と考えているわけではありません。過去の膨大な成功例から、

  • .bak が見つかったサーバーは他にも置き忘れが多い
  • .env.production が見つかる環境は設定管理も甘い
  • .git が公開されているサーバーはソース管理も甘い

という"統計"を利用しているだけです。つまり攻撃者は、サーバーを見ているようで、その向こうにいる運用者の習慣を見ています。

ソフトウェアの脆弱性だけではなく、人間の癖まで含めてスキャン対象になっているということです。

おわりに

以前は、ソフトウェアの脆弱性を突く攻撃が主流でした。もちろん今もゼロデイ攻撃は存在します。

しかしログを眺めていると、それと同じくらい多いのが

「人間が置き忘れたものを探す攻撃」

です。

  • .env
  • .bak
  • .old

どれも高度な脆弱性ではありません。運用の隙です。だからこそ、防御も特別な技術だけではありません。

  • 不要なファイルを公開ディレクトリへ置かない。
  • 作業が終わったら必ず片付ける。
  • 公開前に一度確認する。

そうした当たり前の積み重ねが、結果として最も強い防御になります。

それを防ぐための筆者の手

/etc/conf_backup

のような、まず公開ディレクトリではないディレクトリを作り、

sudo cp -pi /home/www-data/wordpress/wp-config.php /etc/conf_backup/wp-config.php.conf.$(date +%Y%m%d)

のように別ディレクトリに作ります。(もちろん、/etcがWeb経由で閲覧できる状況になっているのであれば、それは「バックアップが見えるかどうか」を心配する段階ではありません。サーバーそのものの侵害を疑うべき状況です)

また、変数による日付自動付与により

  • 最新版2026-3(承認済み).xlsx
  • 2026確定版(社長の意見反映).xlsm
  • 最新確定版2026_コピー(2)セキュリティ委員修正済み.xlsx
  • 改訂版2026_new_コピー(1)レビュー済み.xlsm
  • 2026-10改訂最新版(消さないこと)重役確認済み.xlsx

のような「だから最新どれだよ」を防ぐことはある程度行えます。ちょっとした工夫で癖は防げるというお話しでした。

【WAF検知ログ解析】難読化された JavaScript 偵察ツールを ModSecurity はどう見抜いたのか

はじめに

Redmine 6.1への移行作業がようやく一段落したので、久しぶりにサーバーログを眺めていました。

すると、トップページ (hoge.example.com) に対して、見慣れない長大なペイロードが飛んできていました。

最近の攻撃は「脆弱性を突く」ことだけが目的ではありません。まずは対象がどんな環境なのかを調べ、その結果によって次の手を変えてきます。

今回は、その偵察段階で送られてきたJavaScriptペイロードがなかなか興味深い内容だったので、ModSecurityのログを追いながら見ていきます。

検出された ModSecurity ログ

まずは実際のログです。

例によってIPアドレスはダミーに置き換えています。テロリストに名前を与えていません。

[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Warning. Pattern match "(?i)(?:b[\\"'\\\\)\\\\[\\\\x5c]..." at ARGS:0. [file "REQUEST-932-APPLICATION-ATTACK-RCE.conf"] [line "205"] [id "932235"] [msg "Remote Command Execution: Unix Command Injection (command without evasion)"] [data "Matched Data: eval)(global[String['from'+'CharCode'](66,117,102,102,101,114)].from('KGFzeW5jIGZ1bmN0aW9uKCl7Ci8vIGZhc3RfcmVjb25fdjY..."] [hostname "hoge.example.com"] [uri "/"]
[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Rule [id "932250"] - Execution error - PCRE limits exceeded (-47): (null). [hostname "hoge.example.com"] [uri "/"]
[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Warning. Pattern match "(?i)\\\\b\\\\(?[\\"']*(?:assert|eval|exec|passthru)..." at ARGS:0. [file "REQUEST-933-APPLICATION-ATTACK-PHP.conf"] [line "406"] [id "933160"] [msg "PHP Injection Attack: High-Risk PHP Function Call Found"] [hostname "hoge.example.com"] [uri "/"]

最初に目を引くのは Rule 932235 が反応していることです。

Remote Command Execution:
Unix Command Injection

さらに途中では

PCRE limits exceeded (-47)

というログも出ています。

Base64の中身を見てみる

ログ中に埋め込まれていたBase64文字列をデコードすると、次のようなヘッダーが現れました。

(async function(){
// fast_recon_v6 — signature-rotated recon payload
// Changes from v5:
//   - Randomized top-level JSON keys (no fixed schema to fingerprint)
//   - Variable output structure per invocation
//   - IMDS calls use randomized User-Agent + jittered timeouts
//   - File reads are shuffled order (no deterministic sequence)
...

名前からも分かるように、これは偵察を目的としたJavaScriptです。面白いのは「何を調べるか」よりも、「どうやってWAFの検知を避けようとしているか」の方でした。

難読化の目的はコードを隠すことではない

例えば、

global[String['from'+'CharCode'](...)]

という書き方。普通なら

Buffer

と書けば済むものを、わざわざ文字コードから組み立てています。さらに

(0,eval)(...)

という間接呼び出しも使われています。どちらもJavaScriptとしては動作は同じですが、単純な文字列検索やシグネチャ検知を避けるための定番手法です。

コードを読めなくすることより、「機械に見つかりにくくすること」の方が目的と言った方が近いでしょう。

偵察対象も今どきらしい

コードを見ると、次のような情報収集も行おうとしていました。

  • クラウドメタデータ(IMDS)へのアクセス
  • IAMロールなど認証情報の取得
  • ファイル探索順序のランダム化
  • User-Agentやタイムアウト値のランダム化

固定パターンをできるだけ作らないよう工夫されており、「同じ攻撃を毎回少しずつ変える」という最近の攻撃らしい作りになっています。

PCRE が限界を迎えても、防御は止まらない

途中で

PCRE limits exceeded (-47)

が記録されています。これは巨大な入力に対して、ある正規表現がバックトラック上限へ達したことを示しています。

しかし、OWASP CRSは一つの巨大なルールだけで防御しているわけではありません。このリクエストでは、

  • 932235
  • 932260
  • 933160
  • 933210

と、別方向から検査するルールが続いてヒットしました。

つまり、一つの正規表現が処理を諦めても、他のルールが引き継ぐ設計になっています。

多層防御という言葉はよく聞きますが、今回のログはその動きを非常に分かりやすく見せてくれました。

おわりに

最近は攻撃そのものより、「検知をどう回避するか」の工夫を見る機会が増えてきました。

とはいえ、こうして落ち着いて中身を眺められるのは、防御基盤が先に仕事をしてくれているからです。

「サーバー運用はサファリパークに似ている」ようなものです。猛獣を間近で観察できるのは面白いですが、それは檻や装甲車があるからこそです。

今回も同じでした。難読化されたJavaScriptをじっくり読めたのは、攻撃がアプリケーションまで届かなかったからです。

運用者としては、その「観察できる余裕」こそが、一番ありがたい成果だったのかもしれません。

WasabiをRedmineの添付ファイル保存先にしたら、画像の貼り付けだけ「早期削除」が気になり始めた話

Redmine の添付ファイル保存先として、Wasabi オブジェクトストレージを s3fs でマウントして利用しています。

運用自体は安定していましたが、ある日 Wasabi のダッシュボードを眺めていて違和感を覚えました。

Timed Object Storage(早期削除)の対象が、思ったより増えている。

以前、Nextcloud や Growi とオブジェクトストレージの組み合わせで、Timed Object Storage に長期間悩まされた経験(150日の亡霊)があります。

そのとき学んだのは、「動いている」ことと、「そのストレージに適した運用」であることは別だということでした。

その経験があったからこそ、今回も「また何か起きているのではないか」と考え、一つずつ確認してみることにしました。

添付方法によって違いがあるのでは?

まず比較したのは、画像の添付方法です。

確認した結果、少なくとも現時点では次のような傾向が見えました。

添付方法Timed Object Storage の発生
ファイル選択からアップロード確認できず
Ctrl+Vでクリップボード貼り付け確認
Enhanced UIなどの貼り付け機能確認

もちろん、これだけで原因が断定できるわけではありません。「画像を添付する」という同じ操作でも、添付方法によって内部処理が違う可能性は十分考えられます。

クリップボード貼り付けでは一時ファイルを生成・削除しているのかもしれませんし、Enhanced UI 側の実装が影響しているのかもしれません。

現時点では、そこまで踏み込んだ検証はできていません。

今回試してみる対策

以前の経験から、オブジェクトストレージへ直接細かなアクセスを繰り返す構成は、あまり相性が良くないと感じています。

そこで今回は、s3fs のキャッシュ機能を利用して、ローカル SSD をワンクッション挟む構成へ変更してみることにしました。

追加した主なオプションは次の3つです。

  • use_cache
  • stat_cache_expire=60
  • enable_content_md5

まずキャッシュディレクトリを作成します。

sudo mkdir -p /var/cache/s3fs_bucket_a /var/cache/s3fs_bucket_b
sudo chown -R www-data:www-data /var/cache/s3fs_bucket_a /var/cache/s3fs_bucket_b
sudo chmod 750 /var/cache/s3fs_bucket_a /var/cache/s3fs_bucket_b

続いて /etc/fstab を更新します。

cd /etc
sudo cp -pi fstab /etc/conf_backup/fstab.20260830
sudo nano fstab
--- /etc/conf_backup/fstab.20260830     2025-08-07 11:01:54.000000000 +0900
+++ fstab                               2026-08-30 19:58:37.000000000 +0900
@@ -2,8 +2,9 @@
 LABEL=BOOT      /boot   ext4    defaults        0 2
 LABEL=UEFI      /boot/efi       vfat    umask=0077      0 1
 /swapfile       none    swap    sw      0 0
-# Wasabi Bucket A (storage.example.com)
-s3fs#storage.example.com /mnt/wasabi fuse _netdev,allow_other,passwd_file=/home/sampleuser/.passwd-s3fs,url=https://s3.ap-northeast-1.wasabisys.com,use_path_request_style,uid=33,gid=33 0 0
 
-# Wasabi Bucket B (counter.example.org)
-s3fs#counter.example.org /mnt/wasabi2 fuse _netdev,allow_other,passwd_file=/home/sampleuser/.passwd-s3fs,url=https://s3.ap-northeast-1.wasabisys.com,use_path_request_style,uid=33,gid=33 0 0
+# Wasabi Bucket A (storage.example.com - ap-northeast-1)
+s3fs#storage.example.com /mnt/wasabi fuse _netdev,allow_other,passwd_file=/home/sampleuser/.passwd-s3fs,url=https://s3.ap-northeast-1.wasabisys.com,use_path_request_style,uid=33,gid=33,use_cache=/var/cache/s3fs_bucket_a,stat_cache_expire=60,enable_content_md5 0 0
+
+# Wasabi Bucket B (counter.example.org - ap-northeast-1)
+s3fs#counter.example.org /mnt/wasabi2 fuse _netdev,allow_other,passwd_file=/home/sampleuser/.passwd-s3fs,url=https://s3.ap-northeast-1.wasabisys.com,use_path_request_style,uid=33,gid=33,use_cache=/var/cache/s3fs_bucket_b,stat_cache_expire=60,enable_content_md5 0 0

変更後は再マウントします。(daemon-reloadしないと怒られました)

sudo umount /mnt/wasabi
sudo umount /mnt/wasabi2
sudo systemctl daemon-reload
sudo mount /mnt/wasabi
sudo mount /mnt/wasabi2

これで本当に改善するのか?

正直なところ、この記事を書いている時点ではまだ分かりません。今回の変更は、「クリップボード貼り付け時の一時ファイルが原因ではないか」という仮説に基づく対策です。

実際に Timed Object Storage の発生が止まるのか、それとも別の要因があるのかは、しばらく Wasabi のダッシュボードを見ながら経過観察する必要があります。

もし改善が確認できれば追記しますし、変化がなければ別の原因を探ることになります。

まとめ

今回の目的は、「原因を突き止めた」という報告ではありません。

Wasabi のダッシュボードで小さな違和感を見つけ、その原因として添付方法の違いに着目し、対策を試し始めたという記録です。

以前、オブジェクトストレージとの組み合わせで大きく痛い目を見た経験があるからこそ、「いつもと違う」を見逃さずに済みました。

サーバー運用では、エラーが出てから対応するよりも、違和感の段階で調べ始める方が結果として被害は小さく済みます。

今回の変更が正解かどうかは、これからの経過観察で判断したいと思います。

【Redmine 6.1移行】DBマイグレーションで「Table already exists」連発? 本体統合された機能との競合を解消して移行を完走した記録

Redmine 5.1からRedmine 6.1への移行では、できるだけ本番環境へ影響を出さないよう、検証環境で一つずつ作業を進めてきました。

今回は以前のように「一気にアップグレードして問題を追う」のではなく、

  • 検証環境を作る
  • 問題が起きたら原因を調べる
  • 記録として残す
  • 次の工程へ進む

という流れで進められたため、最終的にはかなり安心して6.1環境を完成させることができました。

その途中で遭遇したのが、データベースマイグレーション時の Table already exists エラーです。

最初は単純なテーブル重複かと思いましたが、原因を追っていくとRedmine 6.0で行われた「プラグイン機能の本体統合」が関係していました。

今回は、このマイグレーションエラーの内容と対処手順を記録しておきます。

実データを流し込んだ直後にマイグレーションが止まる

今回の手順では、Redmine 5.1で整理・純化しておいたデータベースをMySQLダンプから復元し、そのままRedmine 6.1側でマイグレーションを実行しました。

cd /home/www-data/redmine_v6
sudo -u www-data RAILS_ENV=production bundle exec rake db:migrate

ところが途中で処理が停止します。

最初に止まったのは、リアクション機能です。

== 20250423065135 CreateReactions: migrating ==================================
-- create_table(:reactions)

Mysql2::Error:
Table 'reactions' already exists

この時点では、

「どこかでマイグレーションを実行し忘れたかな?」

程度に考えていました。ところが、修正して再実行すると、今度はこちら。

== 20250611092155 CreateDoorkeeperTables: migrating ===========================

Mysql2::Error:
Table 'oauth_applications' already exists

また別のテーブルが既に存在すると言われます。つまり偶然ではなく、何か共通した原因がありそうでした。

原因は「昔はプラグイン、今は標準機能」

調べてみると、どちらもRedmine 6.0で本体へ取り込まれた機能でした。

今回衝突したのは、

  • リアクション機能
  • OAuth認証(Doorkeeper)

の二つです。どちらも以前はプラグインとして利用していましたが、Redmine 6ではコア機能になっています。つまり、

Redmine 5.1時代

プラグイン
    ↓
DBにテーブル作成

だったものが、Redmine 6.1では、

Redmine本体
    ↓
同じ名前のテーブルを作成

という流れに変わっています。そのため、旧環境からDBを持ってくると、

既に存在するテーブルを、Redmine本体がもう一度作ろうとする

という状態になっていました。

なぜDROPしてよいのか

ここで少し悩みました。

「既存テーブルを削除してしまって本当に大丈夫なのか?」

しかし今回は、本体側へ正式に統合された機能です。つまり最終的に利用するのはRedmine本体が管理するスキーマになります。古いプラグイン時代のテーブルを残していても、最終的には使われません。

そこで今回は、競合しているテーブルだけを削除し、本体マイグレーションに改めて生成してもらうことにしました。

競合しているテーブルを削除する

MySQLから競合しているテーブルを削除します。

mysql -u redmine_v6 -p redmine_v6 -e "
DROP TABLE IF EXISTS reactions;
DROP TABLE IF EXISTS oauth_access_tokens;
DROP TABLE IF EXISTS oauth_access_grants;
DROP TABLE IF EXISTS oauth_applications;
"

これで、本体側が新しくテーブルを作れる状態になります。

改めてマイグレーションを実行する

続いて再度マイグレーションを実行します。

cd /home/www-data/redmine_v6
sudo -u www-data RAILS_ENV=production bundle exec rake db:migrate

今度は問題なく進みます。

== CreateReactions: migrated
== EnsureWikiTablesortSettingIsStoredInDb: migrated
== CreateDoorkeeperTables: migrated

最後まで完走し、正常終了しました。

プラグイン側も忘れずに更新

本体が終わったら、続いてプラグイン側のマイグレーションも実行します。

sudo -u www-data RAILS_ENV=production bundle exec rake redmine:plugins:migrate

本体だけ更新して安心しがちですが、ここまで実行して初めてプラグイン側も新しい環境へ追従できます。

configuration.ymlも忘れずに引き継ぐ

今回の環境ではSMTP設定も引き継ぐ必要がありました。

既存環境から configuration.yml をコピーします。

sudo -u www-data cp -p \
/home/www-data/redmine/config/configuration.yml \
/home/www-data/redmine_v6/config/configuration.yml

その後、Passengerを再起動します。

sudo touch /home/www-data/redmine_v6/tmp/restart.txt
sudo systemctl reload apache2

最後に管理画面からテストメールを送信し、Zoho Mail経由で正常に届くことまで確認しました。

移行後の確認

今回の検証では、最終的に以下を確認できました。

  • 過去のチケット・Wiki・添付ファイルを正常に閲覧できる
  • ガントチャートも問題なく表示される
  • kodomo テーマもRedmine 6.1環境で正常動作
  • テストメールを送信し、Zoho Mailで受信できることを確認
  • DMSFを利用しなくても、標準添付機能で画像を配置できることを確認

ここまで確認できれば、検証環境としては十分安心できる状態になりました。

今回の移行を振り返って

今回のRedmine 6.1移行では、「作業そのもの」よりも「途中で何が起きたか」を残しながら進められたことが大きかったように思います。

以前であれば、エラーを解消して先へ進むことを優先していた場面でも、

  • なぜ起きたのか
  • Redmine側の仕様変更なのか
  • プラグイン由来なのか
  • 次に同じ作業をするとき、何を確認すればよいのか

という視点で整理しながら進められました。

結果として、今回遭遇した Table already exists も「たまたま起きたエラー」ではなく、Redmine 6でプラグイン機能が本体へ統合されたことによる仕様変更だと理解できました。

移行作業では、どうしてもエラーそのものへ目が向きがちですが、「なぜそのエラーが起きたのか」まで追っておくと、次回以降の作業はずっと楽になります。

今回の6.1環境は、そうした記録を積み重ねながら構築できたこともあり、これまでで一番安心して仕上げられたバージョンアップだったように感じています。

Page 1 of 2

Powered by WordPress & Theme by Anders Norén