この記事について
この記事ではコメント機能などのUGCサービスを提供する人であれば欠かすことのできないプロバイダ責任制限法についてサービス設計・エンジニアリング観点から対応方針を検討してみようと思います。
なお、この記事はなるべくエンジニア業務に携わらない人でもわかるように書いていますが、後半の投稿者情報保存の部分はプログラムコードも書いています。その部分だけ飛ばしても問題無いように書いていますが、もしも不安がありましたら以下の記事を合わせて読んでみてください。下記記事が分かる方であれば理解できるようにプログラムコードを書いています。
プロバイダ責任制限法とは
名誉毀損表現、著作権法違反など、違法情報が投稿された時に、その投稿を削除を施したり、被害を受けた方から投稿者情報の開示を求められた時に開示することに関しての規定を定めたものです。
「そんなの特に気にせず削除・開示しちゃえばいいじゃん!」という声も多いかと思うのですが、削除に関しては投稿した人の表現の自由の問題があったり、開示に関しては投稿者のプライバシーの問題があるためこのような法律が定められています。
また、大規模なサービスであればあるほど、違法情報が投稿されたことについて自主パトロールのみで気づくことが難しいため、情報を流通させてしまった際のサービス運営者の免責事項を定めて安心してサービス運営できるようにしているという意義も存在します。
この法律には主に以下のことが定められています。なお、正確な解釈についてはお近くの弁護士事務所などの法律専門家に相談するようにしましょう。
削除について
- 当該削除対象とされる違法情報の存在を知らない、知っていても違法情報と認めるに足りる相応の情報を知らないとき、サービス運営者の情報公開責任の免責(3条1項)
→削除しなかったときの被害者に対してのサービス運営者の責任を免除する規定です。 - 削除の申し立てを受けた時、以下の時であれば削除しても、投稿者に対しての損害賠償責任の免除
- 違法であると信じるに足りる相応の理由がある時(3条2項1号)
- 投稿者に削除して良いかの連絡して7日以上返信がなかった時(3条2項2号)
開示について
- 以下の条件をすべて満たせば被害者がから投稿者情報の開示を求めたとしても投稿者情報を開示しても免責される
- 被害者の権利を侵害していることが明らかなとき(4条1項)
- 損害賠償の請求など、開示をするにあたっての正当な理由がある時(4条1項)
- 投稿者に開示して良いかの連絡をして意見を聞いている(投稿者とそもそも連絡が取れない、返信がないなど場合は除く)(4条2項)
- 情報開示を受けた者は開示情報を必要以上にみだりに利用することの禁止(4条3項)
→投稿者のプライバシー保護を規程したものです。
なお投稿者情報として開示可能な項目については省令で以下8項目が定められております。
- 氏名・または名称
- 住所
- 電話番号
- 電子メールアドレス
- 投稿時のアクセス元のIPアドレスとポート番号
- 携帯端末識別符号
- SIMカード識別符号
- 投稿時のタイムスタンプ
サービス設計・システム構築上のポイント
削除について
主にサイト設計的なところが重要と思われます。大きく2ポイントと思われます。
お問い合わせフォームを作る
一応該当情報が違法であると知らなかった場合は、免責される法律が定めてあります。しかし全く無対策はユーザーの信頼を損なう恐れがあります。そのためお問い合わせフォームなどを作って情報提供ができるようにしましょう。
利用規約を作る
こちらも相応な理由があれば、削除しても投稿者に対しての責任は免除されるように法律が定めています。ただし、違法情報であるという判断はなかなか難しいかと思います。また、サイトによっては違法情報でなくても、運営上好ましくない投稿もあると思います。事前に利用規約を作り、その利用規約にそぐわない投稿はサイト管理者が削除できる利用規約を設けると良いでしょう。
開示について
2020/10の現時点では投稿者情報の保存義務は法律上規定がありません。しかしながら、昨今誹謗中傷投稿が問題視されており、投稿者情報保存義務の法律規定の声もあります。そのためサイトの信頼性保持のために投稿者情報も保持し開示準備もできるようにしたほうがいいでしょう。
なお、開示に関する免責は一応法律で規定はされていますが、プライバシーポリシーなどの利用規約で開示に関して規定をしておくとよりユーザーに対して親切かつ、訴訟リスク防止として安全かと思われます。
前述のように開示項目は8つあります。
氏名・住所・メールアドレス・電話番号は会員制サイトであれば持っていることが多いと思われます。しかしながら、ブログのコメントや掲示板など会員でなくても投稿を許可するWebサイトについては別途対策をとらないといけません。かといって、氏名、住所、メールアドレスといった情報をこの法律への対応のために別途ユーザーから取得させるのはサイト設計上ハードルがとても高いです。
このケースではIPアドレスとポート番号、タイムスタンプの保持が重要になります。これらの保持について検討してみましょう。
以下2つの実装方針が考えられます。
アクセスログの中に保存する
メリット
- 小さなシステムであるならばデータベース保存と比べると実装コストが低い。
デメリット
- 省令が定める項目全部となると保存が大変。※一般的なOSSがデフォルトで保存してくれるものはせいぜい投稿元のIPアドレスのみ。
- サーバ複数台運営になったり、データが膨大になると保存管理が大変。※Splunkなどサーバごとのログ集約するシステムが別途必要になる。
- 検索性が悪い。フロントエンドサーバがDBに直接アクセスせず、バックエンドサーバを通してDBをアクセスする仕組みだと突合が難しくなる。
- エンジニア職でない人が特定できるようにしたい場合は結局管理ツールを作らなければならず、実装コストはたくさんかかってしまう。
投稿情報と一緒にデータベースに保存する
メリット
- 検索性良く、特定が容易。
- 管理者ツール作成も容易。
デメリット
- 小さなシステムだと実装はアクセスログよりもコストがかかる可能性がある。
- フロントエンドが直接DBにアクセスせず、バックエンドサーバが管理する場合、バックエンドに送る情報量が増えて実装が面倒になる。
それぞれのパターンで実装が容易なシステムアーキテクトを図で表すとこうなります。
投稿者情報保存の実装例
それぞれの実装例を簡単に示してみます。今回はWebの仕組みがわかる JavaScriptで覚える教養としてのサーバサイドプログラミング入門で作成したチャットアプリに実装してみます。
なお、IPアドレスについて、投稿元IPだけでなく、接続先IP、つまりアクセスを受け付けたサーバのIPも含むべきということが政府で議論されていますので今回は接続先IPの保存にもチャレンジしてみます。
アクセスログに保存
最初のメリデメにも書きましたが、日本の法律で定める項目全て実装しようとすると、OSSのライブラリがほとんど対応していません。カスタマイズすればできなくもないですが、初心者・入門者向けの実装を考慮して自前でアクセスログを作ることにします。
var express = require("express");
var app = express();
//ログファイル出力のためにfsというライブラリを読み込みます
var fs = require('fs');
//サーバを起動する前に自身のIPアドレスを取得します。
var os = require('os');
//このメソッドでIPアドレスが取れますが一般的に複数あるため、サーバとして開放しているIPアドレスを探します。
var interfaces = os.networkInterfaces();
//様々なIPアドレスの名前がありますが、今回は自宅環境を想定しているのでen0という名前のIPを取得します。
var serverIp;
for (var dev in interfaces) {
interfaces[dev].forEach(function(details){
if (details.family == "IPv4" && dev === "en0") {
serverIp = details.address;
}
});
}
//全てのアクセスポイントにログ出力処理を書くのは辛いのでmiddlewareという機能を使います。
app.use(function(req, res, next){
//タイムスタンプ取得
var timestamp = new Date().toISOString();
//アクセス元IP取得
var remoteIp = req.connection.remoteAddress;
//アクセス元ポート取得
var remotePort = req.connection.remotePort
//ログの文字列作成
var log = timestamp + " From=" + remoteIp + " " + remotePort + " To=" + serverIp + "\n";
//ファイルに書き込みます
fs.appendFile('access.log', log, function(){});
next();
});
//以下アクセスURLごとの実装定義です。
//フォームによるPost送信のデータを受け取る設定
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({
extended: true
}));
var list = [];
//viewエンジンをejsであることを設定
app.set("view engine", "ejs");
//アクセスを受け取るURLを指定します。
app.get('/', function(req, res){
res.render("index", {list: list})
});
//投稿を受け付ける処理を書きます
app.post('/', function(req, res){
//日付を表示するため日付オブジェクトを作成します。
var date = new Date();
//受け取った値を追加
list.push({
date: date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate() + " " + date.getHours() + "時" + date.getMinutes() + "分",
comment : req.body.comment
});
res.render("index", {list: list})
});
app.listen(80, function() {
console.log("サーバー起動中");
});
実行して、アクセスしていくと、同じフォルダ内のaccess.logというファイルにアクセスログが溜まっていきます。
% cat access.log
2020-10-10T18:20:52.792Z From=::ffff:192.168.2.112 50863 To=192.168.2.110
2020-10-10T18:21:06.624Z From=::1 58399 To=192.168.2.110
2020-10-10T18:21:14.860Z From=::1 58400 To=192.168.2.110
2020-10-10T18:20:52.792Z From=::ffff:192.168.52.212 59863 To=192.168.2.110
2020-10-10T18:20:52.792Z From=::ffff:192.164.12.712 50968 To=192.168.2.110
2020-10-10T18:20:52.792Z From=::ffff:192.162.23.132 51863 To=192.168.2.110
投稿情報と一緒にデータベースに保存
データベースとして保存する場合は以下のように実装します。このチャットアプリではlistという変数配列で簡易データベースを作っているため、文字列の配列からオブジェクトの配列としてデータ保存構造を変えるだけで終わっています。しかしながらMySQLなどRDBを使う場合はテーブルの変更、SQLの変更も必要で修正項目はもっと増えます。
ちなみに、DB型はデータ利用が容易であるため管理者用の閲覧ページも作っています。
var express = require("express");
var app = express();
//サーバを起動する前に自身のIPアドレスを取得します。
var os = require('os');
//このメソッドでIPアドレスが取れますが一般的に複数あるため、サーバとして開放しているIPアドレスを探します。
var interfaces = os.networkInterfaces();
//様々なIPアドレスの名前がありますが、今回は自宅環境を想定しているのでen0という名前のIPを取得します。
var serverIp;
for (var dev in interfaces) {
interfaces[dev].forEach(function(details){
if (details.family == "IPv4" && dev === "en0") {
serverIp = details.address;
}
});
}
//以下アクセスURLごとの実装定義です。
//フォームによるPost送信のデータを受け取る設定
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({
extended: true
}));
var list = [];
//viewエンジンをejsであることを設定
app.set("view engine", "ejs");
//アクセスを受け取るURLを指定します。
app.get('/', function(req, res){
res.render("index", {list: list})
});
//投稿を受け付ける処理を書きます
app.post('/', function(req, res){
//日付を表示するため日付オブジェクトを作成します。
var date = new Date();
//タイムスタンプ取得
var timestamp = date.toISOString();
//アクセス元IP取得
var remoteIp = req.connection.remoteAddress;
//アクセス元ポート取得
var remotePort = req.connection.remotePort
//受け取った値を追加
list.push({
timestamp : timestamp,
serverIp : serverIp,
remoteIp : remoteIp,
remotePort : remotePort,
date: date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate() + " " + date.getHours() + "時" + date.getMinutes() + "分",
comment : req.body.comment
});
//もしもバックエンドサーバがDB管理している場合以下のように全ての値をバックエンドに送る必要があります。
/**
var axios = require("axios");
var url = ""http://backend.com;
axios.post(url, {
timestamp : timestamp,
serverIp : serverIp,
remoteIp : remoteIp,
remotePort : remotePort,
date: date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate() + " " + date.getHours() + "時" + date.getMinutes() + "分",
comment : req.body.comment
});
*/
res.render("index", {list: list})
});
//必要に応じて管理者ページを作る必要があります。
app.get('/admin', function(req, res){
res.render("admin", {list: list})
});
app.listen(80, function() {
console.log("サーバー起動中");
});
なお管理者ツールはこんな見た目になります。
以上が実装イメージです。ミニマムなチャットアプリと比べるとデータ取得のためにいろいろ処理をいれないといけないことがわかります。また、フロントエンド、バックエンドと階層構造のサーバの場合、各層ごとに法律対応用の処理を書かなければいいけないため、それだけ実装コストが増えてしまいます。
おわりに
この記事を通じて少しでもサイト設計でプロバイダ責任制限法を意識できたり、エンジニアリングやサイト設計に携わっていない方がプロバイダ責任制限法対応のためにどのような対応をサイト運営者が行うのかイメージできたら幸いです。
電気通信事業法の通信の秘密、OSSのライセンス、そして今回のプロバイダ責任制限法など、エンジニアの立場共通でも法律の内容や契約の中身について理解する必要があったり、逆に法務の立場でもシステムの仕様にコミットする場面が多々あると思います。今後もいくつか記事にしてみようと思います。
参考サイト・参考文献
Photo by David Veksler on Unsplash