ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

Ansibleで、サーバー上の特定ディレクトリにあるファイル名一覧を取得する

個人のサーバーでgit運用したくて、Gitbucket を立ててるんです。
それで、この前 Gitbucket の更新をするときにプラグインファイルも更新をかけたのですが、
困ったことに、プラグインをダウンロードするだけのAnsibleタスクだと
古いバージョンのプラグインファイルもディレクトリに残ってしまってバグるんです。

なので、古いプラグインファイルを削除するためにどうしようかと考えて、
ダウンロードするプラグインファイル名一覧と
現在サーバーにあるプラグインファイル名一覧を比較して、それが異なる場合には、
ダウンロード前にサーバー上のプラグインディレクトリを一度削除することにしました。

やり方が少しややこしいのでここに書き残しておきます。
(Ansibleで配列を扱うのは苦労が多いので、AnsibleでがんばるよりはFabricなりCapistranoなりでデプロイを別におこなうのが本当は健全)

1. もともとのタスク構成

こうなってました。

- name: create plugins directory
  become: true
  become_user: "{{ main_user_name }}"
  become_method: sudo
  file:
    path: "{{ gitbucket_plugins_dir }}"
    state: directory

- name: download plugins
  become: true
  become_user: "{{ main_user_name }}"
  become_method: sudo
  get_url:
    url: "{{ item.url }}"
    dest: "{{ gitbucket_plugins_dir }}"
    mode: 0755
  with_items: "{{ gitbucket_plugins }}"
  notify: restart gitbucket

変数 gitbucket_plugins は以下のとおりです。

gitbucket_plugins:
  - name: emoji
    url: https://github.com/gitbucket/gitbucket-emoji-plugin/releases/download/4.4.0/gitbucket-emoji-plugin_2.12-4.4.0.jar
  - name: gist
    url: https://github.com/gitbucket/gitbucket-gist-plugin/releases/download/4.9.0/gitbucket-gist-plugin_2.12-4.9.0.jar

そしてサーバー上には path/to/plugins/ ディレクトリに以下のファイルが入っているとします。

gitbucket-emoji-plugin_2.12-4.4.0.jar
gitbucket-gist-plugin_2.12-4.8.0.jar

このままタスクを実行すると、path/to/plugins/ ディレクトリには

  • gitbucket-emoji-plugin_2.12-4.4.0.jar
  • gitbucket-gist-plugin_2.12-4.8.0.jar
  • gitbucket-gist-plugin_2.12-4.9.0.jar

が存在してしまい、バージョン違いのプラグインが同居して挙動がおかしくなってしまう。そういうわけです。

2. map で解決

そういうわけで、create plugins directory の前に以下のようなタスクを定義することで解決させました。

- name: check current plugins
  become: true
  become_user: root
  find:
    path: "{{ gitbucket_plugins_dir }}"
  register: current_plugins

- set_fact:
    current_plugins_list: >
      {{ current_plugins.files | map(attribute='path') | map('basename') | list | sort }}
    install_plugins_list: >
      {{ gitbucket_plugins | map(attribute='url') | map('basename') | list | sort }}

- set_fact:
    is_gitbucket_plugins_updated: >
      {{ current_plugins_list != install_plugins_list }}

- name: remove plugins directory (when plugins updated)
  become: true
  become_user: "{{ main_user_name }}"
  become_method: sudo
  file:
    path: "{{ gitbucket_plugins_dir }}"
    state: absent
  when: is_gitbucket_plugins_updated

強引な力技ですね・・・(笑)
では、内容を説明していきます。

まず check current plugins にて find を用い、その結果を変数 current_plugins に渡します。

current_plugins.files は、サーバー上のプラグインディレクトリにあるファイルすべての stat を配列で持ちます。

[
  {
    'path': 'path/to/plugins/gitbucket-emoji-plugin_2.12-4.4.0.jar',
    'mode': '0755',
    'size': ...後略
  },
  {
    'path': 'path/to/plugins/gitbucket-gist-plugin_2.12-4.8.0.jar',
    'mode': ...後略
  }
] 

欲しいのはファイル名だけですから、配列の各要素に対してキー名を指定してそこだけ取り出します。
それが map(attribute=‘path’) の部分です。

この Filter をかけることで、配列は以下のようになります。

[
  'path/to/plugins/gitbucket-emoji-plugin_2.12-4.4.0.jar',
  'path/to/plugins/gitbucket-gist-plugin_2.12-4.8.0.jar'
] 

そしてこの path からファイル名の部分だけ取り出すため、
Ansible側で用意されている basename Filter を使います。

map と組み合わせることにより、各要素に対して basename Filter をかけることができ、配列は以下のようになります。

[
  'gitbucket-emoji-plugin_2.12-4.4.0.jar',
  'gitbucket-gist-plugin_2.12-4.8.0.jar'
] 

そしてその後に list Filter をかけます。
さっきまで『配列は以下のようになります』と書いていましたが、実際には map Filter が返すのはmapオブジェクトなので、
list Filter をかけなければ配列にはなりません。

最後に、sort による辞書順の並び替えをしておいて、
このあとの配列の比較に備えます。
この配列を変数 current_plugins_list として保存します。

ダウンロードする予定のプラグイン一覧にも似たような処理をかけて
変数 install_plugins_list に配列を保存します。

そしてこの2つの変数の値が異なるときは
変数 is_gitbucket_plugins_updated は True となり、
サーバー上のプラグインディレクトリの削除がおこなわれる。という仕組みです。

3. 感想

だいぶ無理矢理やってて、魔術じみてますね。
Ansibleで配列使うのはしんどいですが、こういうことができるんだなーと思って書きました。

もう少しがんばれば、プラグインフォルダーを削除するのでなく、
不要になったプラグインファイルだけを削除する、というふうに書けそうな気がしますが、
中途半端なところで妥協しています。

個人サーバーなので複雑なことしたくなくて、アプリケーションの展開までAnsibleにやらせてますが、
結果Ansibleのレシピが複雑になってるので、ちゃんとデプロイツール使ったほうがいいなという気がしてくるこの頃です……。

ヤバイサイトのセキュリティはどれだけヤバイのか検証してみた

1. 前置き

先日、このツイートがエンジニア界隈でちょっとだけ話題になりました。

f:id:nekonenene:20170525191036p:plain

https://twitter.com/ymotongpoo/status/866929418073120769

事の発端は、このツイートにあります。

f:id:nekonenene:20170525191441p:plain

https://twitter.com/e_Traning_html5/status/866677588315447297


WebRTCと言えば、ブラウザを介してリアルタイムボイスチャットをするのに用いられる技術、という認識が一般的ですが、
ポケモンGOに使われている技術』という「ん?」な説明をしており、
その一方でこの人はホームページでHTML5, WebRTCに関する教材販売をしていて、
しかしそのサイトのデザインはとうていサイト作りを教える人のものとは思えない物。

というなかなか強烈なキャラクターを見せてくれました。

f:id:nekonenene:20170525192320p:plain

ttp://yours-company.jp


f:id:nekonenene:20170525192447p:plain

https://twitter.com/e_Traning_html5/status/867038207946272768

私の知ってるHTML5と違う・・・。

さて、このようなサイトで気になってくるのがセキュリティです。
…というのも、このようなツイートを見たからです。

f:id:nekonenene:20170526072108p:plain

https://twitter.com/mpyw/status/866962107606089728

このスクショ1枚だけだったので、おそらくHTTPSでなくHTTPで送信している問題を上げているのでしょう。
とはいえ、そんなサイトは現在まだまだあります。

このサイトであれば、他のセキュリティ的な問題があるかもしれない、
と、セキュリティ素人ながら気になって眺めてみました。

2. パスワードの認識されないログイン画面

f:id:nekonenene:20170526072921p:plain

このような購入画面を経て、

f:id:nekonenene:20170526072900p:plain

ログイン画面に行き着きます。

ここでなんとなくメールアドレスに「 a@a.com 」と書いて
パスワードに「aaaa」と書くと通ったので、「たまたま合ってしまった?」と思いました。
ところが、パスワードを「a」としてもやはり通ってしまいます。

f:id:nekonenene:20170526073406p:plain

これは妙です。どういった判定がなされているのか見てみました。

3. コードの中身は?

HTMLにJavaScriptがそのまま貼られているので、コードを追うのに苦労はしません。

function password_cheack(num){
    var err = 0;
    if(num == 0){
        var str = $('#password').val();
        if(str == ""){
            alert("パスワードを入力してください。");
            $('#password').focus(); //フォーカスを合わせる
            return -1;
        }
        return 0;
    }else if(num == 1){
        var str = $('#password').val();
        var str2 = $('#password2').val();
        if(str2 != str){
            err = -1;
            alert("パスワードが違います。");
            $('#password').focus(); 
            $('#password').select(); //入力された文字列を選択状態にする  
        }
        return err;
    }
};

パスワードチェックはこの部分で、
引数 num = 0 のときは入力フォームである #password の値が空白でないか調べ、空白であればアラートを表示し-1。空白でなければ0を返します。

引数 num = 1 であれば、入力フォームである #password と、hiddenフォームである #password2 の値を比較し、一致していなければアラートを表示し-1、一致していれば0を返します。

ということは、パスワードが一致していない場合でも、hiddenフォームを見れば正しいパスワードを見られるわけで、
その時点でこのパスワードチェックは意味をなしていないわけですが、それは置いておくとして、呼び出し側を見てみましょう。

$('#login').click(function(){
    var err = mail_cheack();
    if(err == -1){
        return -1;
    }
    var err2 = password_cheack(0);
    if(err2 == -1){
        return -1;
    }
    check_database(1);
});

password_cheack(0)

そう、必ず引数は 0 を渡していたのです。
これでは、パスワード欄が入力されているかのチェックしかなされず、たとえパスワードが合っていなくとも、ログインが完了してしまう。そういう仕組みなのでした。

4. 漏れる個人情報

前章にてhiddenフォームである #password2 に正しいパスワードが代入されると書きましたが、
では、どのようにパスワードが代入されているのかを見てみましょう。

function check_database(num){
    var key;
    if(num == 0){
        key = $('#number').val();
    }else if(num == 1){
        key = $('#mail_address').val();
        alert(key);
    }else if(num == 2){
        key = $('#password').val();
    }
    var url = 'http://yours-company.jp/School_Site/account/PHP/database_search.php';
    //var url = 'http://yours-company.jp/School_Site/account/PHP/post.php';
                
    $.ajax( {
        url: url,
        dataType: 'html',
        data: {
            search_no: num,
            search_key: key
        },
        success: function( data ) {
            var datas = data.split(',', 7);
            var err_flag = datas[0];
            var search_type = datas[1];
            var search_key = datas[2];
            var number = datas[3];
            var password = datas[4];
            var mail = datas[5];
            var name = datas[6];
                            
            if(err_flag == 0){
                // パスワードのチェック
                $('#paypal').attr('disabled',false);
                $('#password2').val(password);
                $('#mail_address2').val(mail);
                $('#name').val(name);
                $('#userID').val(number);
                var html = "";
                html = "<p>" + name + "様</br />ご購入頂きありがとうございます。</p>";
                html += "<p>お支払い手続き終了後に" + mail + "のアドレスにご確認メールを致します。ご確認の手続き終了後のご送付になります。</p>"
                $("#kakunin").append(html);
                $("#kakunin").show();
                
            }else{
                alert('アカウントが違います。またはアカウント登録がされていません。');
                $('#paypal').attr('disabled',true);
                $('#kakunin').html('');
                $("#kakunin").hide();
            }
        },
        error: function( data ) {
            alert( '読み込み失敗' );
       }
   });
};

check_databaseメソッドにおいて、database_search.php を叩き、
取得した値を変数 data に代入させたのち、 , で区切って配列 datas に格納し、必要な値を取り出し、
hiddenフォームなどに値をセットしています。

ところで、呼び出し元では
password_cheack メソッドのあとに check_database メソッドでしたから、
これでは password_cheack メソッドの段階では #password2 に値はセットされていません。
password_cheack メソッドの引数が 0 なのは、パスワード判定がどうしても失敗することに困った末の苦肉の策なのかもしれません。


さて、「漏れる個人情報」の話に戻りましょう。
ここの非同期通信(Ajax)のメソッドを見るに、
http://yours-company.jp/School_Site/account/PHP/database_search.php
にアクセスすれば容易に値が取得できそうなことがわかります。

見ず知らずの人のアカウントを取得するのは倫理的に良くないので、会員登録をして試してみます。

f:id:nekonenene:20170526081334p:plain

このようにアカウントを取得してみました。

会員番号の値をもとに情報を取得するには search_no = 0,
メールアドレスの値をもとに情報を取得するには search_no = 1,
パスワードの値をもとに情報を取得するには search_no = 2,
ということがコードから読み取れますから、まずはメールアドレスで試してみましょう。

curl "http://yours-company.jp/School_Site/account/PHP/database_search.php?search_no=1&search_key=test@example.com"

このような値が返ってきます。

0,1,test@example.com,856,testtest,test@example.com,テスト 太郎,1

返ってきた値は、左から順に

  • エラーコード : 0
  • search_no : 1
  • search_key : test@example.com
  • 会員番号 : 856
  • パスワード : testtest
  • メールアドレス : test@example.com
  • 会員の姓名 : テスト 太郎

となっています。(一番右の 1 はダミー?)

パスワードが暗号化せずに保存されていることは、 #password2 との値の比較でパスワードの成否判定をしていたことから今さら驚かないですが、
もちろんこれも良くないですね。

ですが、一番良くないのは全会員情報が簡単に引き出せることにあります。

ブルートフォースアタック(総当たり攻撃)を仕掛けずとも、
連番で登録されている会員番号を1から順番に指定して取得を繰り返す、一行で書けるような簡単なシェルスクリプトを書くだけで、
全会員のメールアドレス、パスワード、会員の姓名を取り出せてしまいます。

5. 考えたこと

今回は極端な例ですが、思ったのは、
あまりいろんなサイトで真面目に会員情報を登録するのは怖いな、ということです。

大きな会社ですと情報漏えいはニュースになるくらいなのでまだいいのですが、
DTMで音源を買う関係で、海外サイトや、個人サイトで会員登録をすることも多く、
そういうところで情報漏えいが起きたところで、私(もしくはそのサイトの運営者)は気付くことがないので、ちょっと怖いなと思いました。

それから、JSコードは minify なり uglify をかけておくと、
多少ダメなコードでも、その難読性から少しは脆弱性対策になるな、と、
今回のコードが読みやすかったぶん感じました。

以上、セキュリティは気を付けないと顧客情報が大変って話でした。
ユーザー登録者としてもウェブ開発者としても、こういう話は明日は我が身です……。


ちなみに、フォームで任意のJavaScriptコードを実行できる問題もありました。

f:id:nekonenene:20170526091800p:plain

git利用者に贈る、Subversionがダメな理由

このブログを始めてからだいたい2年が経ちました。
2年前はどんな記事を書いてみたのかと見てみると……

これですね。
このときはプログラミング始めたてで、まずは git の使い方を覚えようとしていたようです。

このように、ここ数年でプログラミングを始めるようになった方は git からスタートしていて、
逆に Subversion ( SVN )を触ったことがない方も多いのではないかと思います。

では、なぜ現在は Subversion でなく git が使われるようになったのでしょう?
数年前、あるプロジェクトに参加したとき Subversion を初めて使ったところ、
「これはたしかに使いづらい……」とわかり、git が広まった理由を感じられましたので、
git に慣れた者にとって Subversion がつらかったところについてまとめたいと思います。

1. コマンドが長い!

1-1. init

ます、バージョン管理のためのディレクトリを作成する init コマンドを比較してみましょう。
すでにプロジェクトフォルダーへ移動しているものとします。
また、今回は Windows PowerShell 上での実行を想定しています。

gitの場合:

git init

svnの場合:

# まずはローカルもしくはサーバー上に、バージョン管理に使うディレクトリを用意します
# git init との比較ですから、今回はローカル環境に管理ディレクトリを作成することにします
mkdir /svn_repos/project_name
svnadmin create /svn_repos/project_name
svn mkdir file:///svn_repos/project_name/trunk file:///svn_repos/project_name/branches  file:///svn_repos/project_name/tags -m "created default directories: trunk, branches, tags"

# バージョン管理に使うディレクトリを作りましたので、それを pull します
# svn では、git pull は svn checkout です
# "まだプロジェクトフォルダー内に何もデータがない場合"は、以下の1行で済みます
svn co file:///svn_repos/project_name/trunk

なお、Windows10 の PowerShell でおこなっていますので、
もしかすると以前のOSですと /svn_repos や file:///svn_repos といった省略型が認識できないかもしれません。
その場合は、 c:/svn_repos や file://localhost/c:/svn_repos に、それぞれ置き換えてください。

また、プロジェクトフォルダー内にデータがある場合は、
チェックアウトする前にプロジェクトフォルダーを一度コピーして隔離しておく必要があります。
こちらをご覧ください : svnコマンドでリポジトリ作成する手順 – 打つか投げるか

1-2. commit

コミットするときのコマンドを比較してみましょう。
ここはほとんど同じですが、基本の流れですので書いておきます。

gitの場合:

ni test.txt # ni : New-Item を意味する、PowerShell 独自のコマンド。bash での touch コマンドに相当
git add .
git commit -m "created a test file"

svnの場合:

# 先ほどチェックアウトしたことでプロジェクトフォルダー内に
# trunk フォルダーができました。データはそのフォルダー内に置いていきます
cd trunk
ni test.txt
svn add *
svn commit -m "created a test file"

1-3. branch

ブランチを作るときのコマンドを比較してみましょう。

なお、Subversionにおいてブランチを作るというのは、
trunk フォルダーのコピーを指定ディレクトリに作る作業です。
したがって、ブランチを作れば作るほど、どんどんバージョン管理ディレクトリ以下の容量は増えていきます。

また、タグもブランチと同様です。
言ってしまえば Subversion にタグやブランチの機能なんてものは無く、
ユーザーが『慣例的な使い方をする』ことで成り立っています。

詳しくはこちら : 今更聞けないSubversionの使い方

Subversion から git へ移行したくてもできない人が語る、
SVNリポジトリの容量がやたらでかくて……」というお話は、こういうところに由来します。
(その他に以下で述べる、ignore がしづらいため無駄なファイルが……、などの理由も加わるでしょう)

gitの場合:

# 方法1 : ブランチを作りつつそのブランチへ移動
git checkout -b new_branch_name

# 方法2 : 上記コマンドが一般的かと思いますが、いちおう素直にやるのなら、
# 以下のように、ブランチを作ってから移動
git branch new_branch_name
git checkout new_branch_name

svnの場合:

svn copy file:///svn_repos/project_name/trunk file:///svn_repos/project_name/branches/new_branch_name -m "create new branch"

このように、ブランチやタグを作る際にも、管理フォルダーへのURLが必要になります。
今回の例のようにローカル環境に管理フォルダーがある場合はまだいいのですが、
これが社内サーバーだったりすると、いちいちURLを探してコピペしてくるのが面倒だったりします。

git remote set-url コマンドのありがたさを理解しました。

2. ローカルコミットができない

先ほど書いたコミット、ほとんど同じと書きましたが、中身は異なります。
git は commit してリモートリポジトリに push するという流れが一般的です。
一方 Subversion では、commit コマンドの段階で、checkout 時に用いたURLへの送信がおこなわれます。

つまり、Subversion の commit は、git の commit と push が一度におこなわれることを指します。
svn push というコマンドは存在しません。

一見、問題ないかに思えますが、チームでおこなうプロジェクトではなかなか困ったことになります。
書いているコマンドが常に完璧なわけではないですが、ロールバックを考えると、
なるべく頻繁にコミットすべきと git 利用者は考えます。
しかし Subversion では、コミットした途端にリモートリポジトリへの同期がおこなわれてしまいます。
当然、これではコンフリクトの機会も多くなり、コミットできない……コンフリクト解消しなきゃ……の作業を繰り返すことになります。

それから、ブランチを作る際も、git と違いローカルにバンバン作れません。
(仕組みを考えると、個人がローカルにブランチを作ることが可能なのですが、面倒なのでたいていリモートリポジトリにそのまま作っているかと思います)
git co -b を打つことがたびたびある git 利用者にとっては、なかなかつらさがあります。

以前Twitter『ブランチを作るのに申請書が必要』ブラック会社談義を見てどういうことかわからないでいましたが、
Subversion でならたしかに各人がバンバン作ると混沌化しますので、その規則が生まれるのもわからなくもないです。

3. ignore しづらい

.gitignore ファイルを作るのとはわけが違います。
これは説明が面倒ですので、Subversion での ignore の設定法は以下の2つの記事をお読みください。

Subversion/svn propset/ignoreの除外リストをファイルで管理 - yanor.net/wiki
SVNで特定のディレクトリ以下の更新を無視する - ふなWiki

設定が複雑なので、ignore の設定が正確におこなわれずに不要なファイルがコミットされてしまうことはよくあります。

4. コードレビューしづらい

GitHub もねぇ、BitBucket もねぇ、GitLab だってあるわけねぇ! の世界です。
調べたら、 CodeBarg というものがあるようですので、
もし会社が Subversion 環境だけどGitHub的コードレビューの慣習を入れたい、という場合は、
これの提案をおこなってみるといいかもしれません。
Codebrag·Subversion/Git対応のコードレビューシステム MOONGIFT

私が参加していたそのプロジェクトではコードレビューは存在しませんでした。
イサギイイネ・・・

5. コマンドだと大変なので結局GUIツールを使わざるを得ない

最初に書きました 1. の理由もあってか、たいていの人は TurtoiseSVN に落ち着きます。
TurtoiseSVN なら、ブランチ・タグの作成やコンフリクトの解消などが、わりとカンタンにおこなえるためです。

バリバリとコマンド入力をおこなうプログラマーの方にとっては、
GUIツールでバージョン管理をしていることで、体が腐っていく感覚が味わえるかもしれません……。

最後に

というわけで、git利用者が Subversion を使ってみたときのつらみについて書いてみました。
Subversion が必ずしも使いにくいかというと、Visual Studio を使っている人にとっては、TurtoiseSVN が統合されているために Subversion の方が手軽だったりする場合もあるようです。

しかし最近の Visual Studio、特に Visual Studio 2015 以降では git もそこそこ手軽に扱えるようになったようですね。
(参考 : Visual Studio ローカルでバージョン管理(git))

Subversion を使っているところには早く git に移行してほしいなと思うしだいです……。