問題解説: 掲示板が機能しない!

お久しぶりです。
北海道大学の小林巧です。

この記事は、1日目のサーバー問題、「掲示板が機能しない!」の解説です。

設問概要

クロスサイト・スクリプティングの脆弱性がある掲示板型Webアプリケーションを修正してもらう。

配点:5点

解説

出題したWebアプリケーションでは、ユーザーからの入力をDBに格納し、そのまま表示する処理を行っているため、HTMLやJavaScriptなど、ブラウザが解釈できるコードを注入することで、アプリケーション作成者が意図しないスクリプトを閲覧者の環境上で実行することができる脆弱性が存在します。

問題文には「改ざんされた」とのみ記載しましたが、ログやDBに格納された投稿から、攻撃者が当該脆弱性を利用してHTMLコードを注入したため、閲覧者がアプリケーションにアクセスする度に改ざんされたかのように表示されてしまう、というトラブルを推測できるか、文字列を表示前に正しく処理するコードを追加できるかの2点を問いました。

アプリケーションの仕組み

post.cgiにPOSTされたデータは、そのままMySQLに格納されます。

script.jsが読み込み時とボタンが押された時にview.cgiにアクセスし、JSONで投稿データを取得します。

以下に、view.cgiとscript.jsのソースコードを貼り付けます。

 

view.cgi

#!/usr/bin/perl

use CGI qw/:standard/;
use  DBI;

my $n = param('n');
# my $key = param('key');


#config
my $mysql_user = 'bbs';
my $mysql_pass = 'toraconbbs';
my $mysql_db   = 'bbs';


sub print_http_header{
    my ($mime,$status) = @_;

    if($status == 200){
        print header(-type=>"$mime",-charset=>'utf-8',-Access-Control-Origin=>'*');
    }else{
        print header(-type=>"$mime",-status=>"$status",-charset=>'utf-8',-Access-Control-Origin=>'*');
    }
}


sub fetch_data{
    my ($max) = @_;
    my $db = DBI->connect("dbi:mysql:$mysql_db" ,$mysql_user ,$mysql_pass);
    my $sql = "SELECT * FROM bbs.data order by `id` desc limit 3;";
    if ($max != 0 ){
        $sql = "SELECT * FROM bbs.data where `id`  < $max order by `id` desc limit 3;"; } my $ret = $db->selectall_arrayref($sql);
    $db->disconnect();

   my $i = 0;
   my $data = "[";
    foreach $row(@{$ret}){
        if ($i !=  0){$data .= ",";}
        $data .= qq/{"id":"$row->[0]","name":"$row->[1]","title":"$row->[2]","datetime":"$row->[3]","body":"$row->[4]"}/;
        $i++;
    }
    $data .= "]";    
    $data =~ s/\\/\\\\/g;

return $data;
}

my $max = 0;
if ($n){$max = $n;}
&print_http_header("application/json",200);
my $ret = &fetch_data($max);
print $ret;

script.js

var posttag = "<div class='post panel panel-default'><div class='panel-heading'><h3 class='panel-title title'></h3><div><p class='namelabel'>by </p><p class='name'></div></div><div class='panel-body'><p class='body'><p class='datetime'></div></div>";
var n = 0;
var lastId =0;



function fetch(startNum) {
	
            $ . ajax( {
                url: "./view.cgi",
                data: "n=" + startNum,
                success: function( data ) {
 			
                        $.each(data,function(i,post){
  			    $(".thread").append(posttag);
                            $(".datetime").eq(n).append(post.datetime);
                            $(".name").eq(n).append(post.name);
                            $(".title").eq(n).append(post.title);
                            $(".body").eq(n).append(post.body);
			    lastId = post.id;
 			    n++;
			
                        });
                },
                error: function( data ) {
                    alert("ERROR!");
                }
            } );
};

function submit(){

    $.ajax({
            type: "POST",
            url: "./post.cgi",
            cache: false,
            data: {
                name: $(".post-name").val(),
                title: $(".post-title").val(),
		body : $(".post-body").val(),
                key: $(".key").val()
            },
            processData: true,

            ready: function() {
                console.log("processing");
            },
            successs: function(s){
            }
}).done(function(){
    $("#modal1").modal('hide');   
    location.reload("true");
});


};






window.onload = fetch();

$(function() {
    $(".fetch").on("click",function(){
        fetch(lastId);
    });

    $(".new").on("click",function(){
        $("#modal1").modal('show');
        return false;
    });
    $(".submit").on("click", function() {
        if ($(".post-name").val() != '' && $(".post-title").val() != '' && $(".post-body").val() != ''){
            submit();
        }
    });

    $(".reload").on("click",function(){
        location.reload();
    });



});



解答例

view.cgiの39行目でJSONを生成しているので、その前に文字列のエスケープ処理を追加します。
例:

   my $data = "[";
    foreach $row(@{$ret}){
        if ($i !=  0){$data .= ",";}
        my $id = escapeHTML($row-&gt;[0]);
        my $name = escapeHTML($row-&gt;[1]);
        my $title = escapeHTML($row-&gt;[2]);
        my $datetime = $row-&gt;[3];
        my $body = escapeHTML($row-&gt;[4]);
        $data .= qq/{"id":"$id","name":"$name","title":"$title","datetime":"$datetime","body":"$body"}/;
        $i++;
    }
    $data .= "]";

また、script.js中で、16行目から20行目で投稿データをHTMLタグに埋め込んでいるのですが、
17行目から20行目のappendをtextにすることでもエスケープ処理が可能です。

 $(".thread").append(posttag); 
        $(".datetime").eq(n).append(post.datetime); 
        $(".name").eq(n).text(post.name);
        $(".title").eq(n).text(post.title);
        $(".body").eq(n).text(post.body); 

datetimeはユーザからの入力に関係なく生成される値なので、解答例ではエスケープしていません。
エスケープ処理を追加することで、HTMLタグを含む投稿がちゃんと表示されるようになります。

対策前:

1

対策後:

2