システムプログラム(第9週): WWWプログラミングとWWWにおけるアクセス制御


電子・情報工学系/システム情報工学研究科CS専攻
新城 靖
<yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2005/No9.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2005/
http://www.coins.tsukuba.ac.jp/~yas/

今日の重要な話

WWW サーバ側のプログラム クライアント側のプログラム アクセス制御

CGIの考え方

WWW サーバは、普通は、ファイルに保存された HTML データやイメージ・データを 読み込み、WWWブラウザに返する。 CGI (Common Gateway Interface) では、ファイルが読み込まれる代わりにプログラムが実行され、その実行結果 がブラウザに返される。

Gateway とは、WWW で使われている HTTP というプロトコルへの入り口という 意味である。 プログラムの実行結果は、普通は、HTML にすることが多いが、普通のテキス トであることもイメージであることもある。

クライアント、WWWサーバ、CGIによるプロセス、ファイル

図? CGIの仕組み

CGI の利用例

CGIの設定(Apache)

情報学類には、次の3台のサーバが動作している。

www.coins.tsukuba.ac.jp
~/public_html 以下を公開、CGI不可
www2.coins.tsukuba.ac.jp
~/public_html 以下を公開、CGI可能
www3.coins.tsukuba.ac.jp
~/secure_html 以下を公開、CGI可能、SSL対応(http: ではなく https:)
いずれのサーバも、 Apache HTTP Server を 用いている。

CGI になるファイル名のパタンの例(設定により、これ以外も可能)。


/cgi-bin/name
/dir/name.cgi

cgi-hello.c

cgi-hello.c は、クライアントに HTML で hello. と返すCGI のプログラムで ある。
   1:	
   2:	/*
   3:	        cgi-hello.c --簡単な CGI のプログラム
   4:	        ~yas/syspro/www/cgi-hello.c
   5:	        Start: 2002/06/23 18:21:34
   6:	*/
   7:	
   8:	extern void print_header();
   9:	extern void print_content();
  10:	
  11:	main()
  12:	{
  13:	        print_header();
  14:	        print_content();
  15:	        exit( 0 );
  16:	}
  17:	
  18:	void print_header()
  19:	{
  20:	        printf("Content-Type: text/html\r\n");
  21:	        printf("\r\n");
  22:	}
  23:	
  24:	void print_content()
  25:	{
  26:	        printf("<HTML><HEAD></HEAD><BODY>\n");
  27:	        printf("hello.\n");
  28:	        printf("</BODY></HTML>\n");
  29:	}
CGI のプログラムは、Apache の場合、標準ではHTTP のヘッダ(一番最初の 「HTTP/1.0 200 OK」 ではなく、2行目以降)を標準出力に出力する所から始 まる。Content-Type: 行は、必ず付ける。空行がヘッダと本分の区切りである。 本文には、この場合は、HTML の文書を置いている。

実行例:

% mkdir ~/public_html/syspro-cgi-examples/ [←]
% cd ~/public_html/syspro-cgi-examples/ [←]
% cp ~yas/syspro/www/cgi-hello.c . [←]
% cc cgi-hello.c -o cgi-hello.cgi [←]
% ./cgi-hello.cgi  [←]
Content-Type: text/html

<HTML><HEAD></HEAD><BODY>
hello.
</BODY></HTML>
% []
CGI のプログラムの場合、拡張しを .cgi にする。 CGI のプログラムを、直接シェルが実行することもできる。(引数の与え方に ついては、後述する。)

WWW ブラウザでは、次の URL を開く。

http://www2.coins.tsukuba.ac.jp/~syspro/2005/No9_files/cgi-hello.cgi

telnet では、次のようにして実行する。

% telnet www2.coins.tsukuba.ac.jp 80 [←]
Trying 130.158.86.5...
Connected to www2.coins.tsukuba.ac.jp.
Escape character is '^]'.
GET /~syspro/2005/No9_files/cgi-hello.cgi HTTP/1.0[←]
[←]
HTTP/1.1 200 OK
Date: Sat, 18 Jun 2005 09:15:54 GMT
Server: Apache/1.3.27 (Unix)  (Red-Hat/Linux) mod_ssl/2.8.12 OpenSSL/0.9.6 DAV/1.0.2 PHP/4.1.2 mod_perl/1.24_01 mod_throttle/3.1.2
Connection: close
Content-Type: text/html

<HTML><HEAD></HEAD><BODY>
hello.
</BODY></HTML>
Connection closed by foreign host.
% []
GET の代りに POST でもよい。
% telnet www2.coins.tsukuba.ac.jp 80 [←]
Trying 130.158.86.5...
Connected to www2.coins.tsukuba.ac.jp.
Escape character is '^]'.
POST /~syspro/2005/No9_files/cgi-hello.cgi HTTP/1.0[←]
[←]
HTTP/1.1 200 OK
Date: Sat, 18 Jun 2005 09:17:59 GMT
Server: Apache/1.3.27 (Unix)  (Red-Hat/Linux) mod_ssl/2.8.12 OpenSSL/0.9.6 DAV/1.0.2 PHP/4.1.2 mod_perl/1.24_01 mod_throttle/3.1.2
Connection: close
Content-Type: text/html

<HTML><HEAD></HEAD><BODY>
hello.
</BODY></HTML>
Connection closed by foreign host.
% []

ステータス行も含むヘッダ(Apache non-parsed headers)

Apache では、標準では、サーバが最初のステータス行「HTTP/1.1 200 OK」を Apache が作成している。Status:という行を含めることで、ステータ ス行を記述することもできる。Status:がない場合、Location: というヘッダがあれば、302 Found、それ以外の場合は、200 OKが返される。それ以外のヘッダも、Apache が自動的に生成して返す。

ステータス行も含めて、完全なヘッダを CGI で作成して返すこともできる。 この方法を、Apache では、nph (non-parsed headers) と呼んでいる。

Apache で、nph を使うには、nph- で始まるファイル名を用いる。こ れを使うと、Apache は、CGI プログラムが作成したヘッダを一切解釈(parse) せず、そのままクライアントに返す。

CGIとフォーム

ブラウザからサーバに情報を送ってほしい時には、 フォーム(form)という機能を使う。

HTML記述例: http://www2.coins.tsukuba.ac.jp/~syspro/2005/No9_files/cgi-printarg.html

<H1><A NAME="cgi-example-get">CGI の GET メソッドを使う例</A></H1>

<FORM ACTION="http://www2.coins.tsukuba.ac.jp/~syspro/2005/No9_files/cgi-printarg.cgi" method="get">
    <P>
    姓: <INPUT type="text" name="lastname">
    名: <INPUT type="text" name="firstname"><BR>
    <INPUT type="radio" name="sex" value="Male"> 男 <BR>
    <INPUT type="radio" name="sex" value="Female"> 女 <BR>
    <INPUT type="radio" name="sex" value="others"> その他 <BR>
    電子メール: <INPUT type="text" name="email"><BR>
    <INPUT type="submit" value="send"> <INPUT type="reset">
    </P>
</FORM>

<H1><A NAME="cgi-example-post">CGI の POST メソッドを使う例</A></H1>

<FORM ACTION="http://www2.coins.tsukuba.ac.jp/~syspro/2005/No9_files/cgi-printarg.cgi" method="post">
    <P>
    姓: <INPUT type="text" name="lastname">
    名: <INPUT type="text" name="firstname"><BR>
    <INPUT type="radio" name="sex" value="Male"> 男 <BR>
    <INPUT type="radio" name="sex" value="Female"> 女 <BR>
    <INPUT type="radio" name="sex" value="others"> その他 <BR>
    電子メール: <INPUT type="text" name="email"><BR>
    <INPUT type="submit" value="send"> <INPUT type="reset">
    </P>
</FORM>

表示例:

CGI の GET メソッドを使う例

姓: 名:


その他
電子メール:

CGI の POST メソッドを使う例

姓: 名:


その他
電子メール:

<FORM></FORM>で括られた部 分が1つのフォームになる。(1つのページに複数のフォームを置くことがで きる。)

<INPUT>の部分が、ブラウザからサーバに送られるデー タを表わす。

send」ボタンが押されると、ブラウザは、HTTPの POST メソッ ドを使って、サーバにデータを送る。

WWW サーバは、それを受け取ると、指定されたプログラムを実行する。実行さ れたプログラムは、 環境変数標準入力 からデータを受け取ることができる。

環境変数

REQUEST_METHOD
データを送るのに使われたメソッド。POST か GET。 GET は、普通のページを得るものと同じ方法で、長いデータは送れない。
QUERY_STRING
URLの指定で「?」以下に指定された文字列。
SCRIPT_NAME
プログラムの名前。
メソッドとして POST が使われた時、標準入力から <FORM></FORM>で指定されたパラメタを受け取ることが でる。

このデータは、「&」で区切られた 「フィールド名=」の並びになっている。

例:

lastname=姓&firstname=名&sex=Male&email=who@u-ust.ac.jp
POSTの場合、環境変数 CONTENT_LENGTH からは、データの長さ が得られる。

メソッドとして、GET が使われた時には、 環境変数 QUERY_STRING に 「&」で区切られた「フィール ド名=」の並びが含まれている。

送られてくるパラメタの中に漢字、 (&自身)、 URLで使えない文字が含まれていた場合、 「%hh( hh は2桁の16進数)」という形になっている。

これを元にもどすにはcgiparse というプログラムや perl の命令 hex() が使われる。

cgi-printarg.c

次のプログラムは、CGI で実行された時に、その引数を表示するものである。 GET メソッド、および、POST メソッドの両方に対応している。
   1:	
   2:	/*
   3:	        cgi-printarg.c -- CGI プログラムに対する引数を表示するプログラム
   4:	        ~yas/syspro/www/cgi-printarg.c
   5:	        Start: 2002/06/23 18:21:34
   6:	*/
   7:	
   8:	#include <stdlib.h>     /* getenv(), malloc() */
   9:	#include <stdio.h>      /* printf() */
  10:	#include <string.h>     /* strlen() */
  11:	
  12:	extern void print_header(void);
  13:	extern void print_content(void);
  14:	extern int make_qcqv( int *qcp, char ***qvp, char *qs );
  15:	extern char *get_query_string();
  16:	extern char *read_query_string();
  17:	extern void safe_printenv( char *name );
  18:	extern void safe_print_string( char *str );
  19:	extern char *html_escape( char *str );
  20:	extern char *decode_hex( char *str );
  21:	extern int countchr( char *s, char c );
  22:	
  23:	main()
  24:	{
  25:	        print_header();
  26:	        print_content();
  27:	        exit( 0 );
  28:	}
  29:	
  30:	void print_header()
  31:	{
  32:	        printf("Content-Type: text/html\r\n");
  33:	        printf("\r\n");
  34:	}
  35:	
  36:	void print_content()
  37:	{
  38:	    char  *query_string ;
  39:	    char **qv ;
  40:	    int    qc ;
  41:	    int    i ;
  42:	
  43:	        printf("<HTML><HEAD></HEAD><BODY><PRE>\n");
  44:	
  45:	        safe_printenv("REQUEST_METHOD");
  46:	        safe_printenv("SCRIPT_NAME");
  47:	        safe_printenv("QUERY_STRING");
  48:	        safe_printenv("CONTENT_LENGTH");
  49:	
  50:	        query_string = get_query_string();
  51:	        printf("query_string:\n");
  52:	        safe_print_string( query_string ); printf("\n");
  53:	
  54:	        if( make_qcqv( &qc, &qv, query_string ) < 0 )
  55:	        {
  56:	            printf("Error while parsing query string\n");
  57:	            exit( -1 );
  58:	        }
  59:	        for( i=0 ; i<qc ; i++ )
  60:	        {
  61:	            char *decoded ;
  62:	            printf("qv[%d]: ",i);
  63:	            safe_print_string(qv[i]);
  64:	            decoded = decode_hex( qv[i] );
  65:	            printf("(");
  66:	            safe_print_string( decoded );
  67:	            printf(")\n");
  68:	        }
  69:	
  70:	        printf("</PRE></BODY></HTML>\n");
  71:	        exit( 0 );
  72:	}
  73:	
これは、標準(nphではない)の CGI プログラムな ので、print_header()では、HTTP のヘッダのうち、Content-Type: 行だけを 出力している。

print_content() では、本文を出力している。

safe_printenv() で、環境変数 REQUEST_METHOD, SCRIPT_NAME, QUERY_STRING, CONTENT_LENGTH の内容を標準出力に出力している。

get_query_string() は、クライアントから送られてきたパラメタ(query string)を読込み、文字列として返す関数である。これを、 safe_print_string() で標準出力に出力している。

make_qcqv() は、クライアントから送られてきたパラメタ(query string)を、 分解して1つひとつバラバラににするものである。分解した結果は、main() の引数である int argc, char *argv[] と同じ形になっている。qc が、argc, qv が argv に対応する。

for ループでは、qv[] をsafe_print_string() で標準出力に出力している。 さらに、decode_hex() で、%hh (%hhは16進数)の形式を、バイトに元に戻すものであ る。

  74:	int make_qcqv( int *qcp, char ***qvp, char *qs )
  75:	{
  76:	    char **qv ;
  77:	    int    qc_max, i, len ;
  78:	    char *s, *p ;
  79:	
  80:	        if( qs == 0 )
  81:	            return( -1 );
  82:	        qc_max = countchr(qs,'&')+1 ;
  83:	        qv = malloc( sizeof(char *)*(qc_max+1) );
  84:	        if( qv == 0 )
  85:	            return( -1 );
  86:	
  87:	        for( i=0 ; i<qc_max ; i++ )
  88:	        {
  89:	            while( *qs == '&' )
  90:	                qs ++ ;
  91:	            if( *qs == 0 )
  92:	                break;
  93:	            for( p = qs ; *p!='&' && *p!=0 ; p++ )
  94:	                continue;
  95:	            /* *p == '&' || *p=='0' */
  96:	            len =  p - qs ;
  97:	            s = malloc( len+1 );
  98:	            if( s == 0 )
  99:	            {
 100:	                int j ;
 101:	                for( j=0 ; j<i; j++ )
 102:	                {
 103:	                    free( qv[j] );
 104:	                    qv[j] = 0 ;
 105:	                }
 106:	                return( -1 );
 107:	            }
 108:	            memcpy( s, qs, len );
 109:	            s[len] = 0 ;
 110:	            qv[i] = s ;
 111:	            qs = p ;
 112:	        }
 113:	        qv[i] = 0 ;
 114:	        *qcp = i ;
 115:	        *qvp = qv ;
 116:	        return( i );
 117:	}
 118:	
make_qcqv() は、「&」で区切られて送られてきたパラメタをバラバラに分解 し、main の引数である int argc, char *argv[] と同じ形にするものである。 まず、「&」の数を先に数えて、argv に相当するメモリを確保する。1つ多 く確保しているのは、最後に0を置くためである。ただし、「&&&」のよう に空のものは取り除いている。

for() 文で1つの「&」の左、または、終端の0の左のまでを切り出している。 qs が先頭、p が「&」、または、終端の0を差すようにする。malloc() で長 さ分(終端の0を含む)のメモリを確保してmemcpy() でコピーしている。 s[len] で終端の0を付け、qv[i] に保存している。qs を q にして、次のルー プに進む。

 119:	char *get_query_string()
 120:	{
 121:	    char *request_method, *query_string;
 122:	        request_method = getenv("REQUEST_METHOD");
 123:	        if( request_method == 0 )
 124:	            return( 0 );
 125:	        else if( strcmp(request_method,"GET") == 0 )
 126:	        {
 127:	            query_string = getenv("QUERY_STRING");
 128:	            if( query_string == 0 )
 129:	                return( 0 );
 130:	            else
 131:	                return( strdup(query_string) );
 132:	        }
 133:	        else if( strcmp(request_method,"POST") == 0 )
 134:	        {
 135:	            return( read_query_string() );
 136:	        }
 137:	        else
 138:	        {
 139:	            printf("Unknown method: ");
 140:	            safe_print_string( request_method );
 141:	            printf("\n");
 142:	        }
 143:	}
 144:	
 145:	char *read_query_string()
 146:	{
 147:	    int   clen ;
 148:	    char *content_length ;
 149:	    char *buf ;
 150:	
 151:	        content_length = getenv("CONTENT_LENGTH");
 152:	        if( content_length == 0 )
 153:	        {
 154:	            return( 0 );
 155:	        }
 156:	        else
 157:	        {
 158:	            clen = atoi( content_length );
 159:	            buf = malloc( clen + 1 );
 160:	            if( buf == 0 )
 161:	            {
 162:	                printf("read_query_string(): no memory\n");
 163:	                exit( -1 );
 164:	            }
 165:	            if( read(0,buf,clen) != clen )
 166:	            {
 167:	                printf("read error.\n");
 168:	                exit( -1 );
 169:	            }
 170:	            buf[clen] = 0 ;
 171:	            return( buf );
 172:	        }
 173:	}
 174:	
get_query_string() は、クライアントから送られてきたパラメタを、1つの 文字列として受け取るものである。GET メソッドの場合()は、単に環境変数 REQUEST_METHOD を読めばよい。ただし、文字列の長さの上限がきつい。

strdup() は、文字列をコピーするものである。使い終わったら、free() で解 放する。

POST メソッドの場合は、read_query_string() で続きの処理を行う。

 175:	void safe_printenv( char *name )
 176:	{
 177:	    char *val ;
 178:	    char *safe_val ;
 179:	
 180:	        printf("%s=",name );
 181:	        val = getenv( name );
 182:	        safe_print_string( val );
 183:	        printf("\n");
 184:	}
 185:	
 186:	
 187:	void safe_print_string( char *str )
 188:	{
 189:	    char *safe_str ;
 190:	
 191:	        if( str == 0 )
 192:	        {
 193:	            printf("(null)");
 194:	            return;
 195:	        }
 196:	        safe_str = html_escape( str );
 197:	        if( safe_str == 0 )
 198:	        {
 199:	            printf("(no memory)");
 200:	        }
 201:	        else
 202:	        {
 203:	            printf("%s",safe_str );
 204:	            free( safe_str );
 205:	        }
 206:	}
 207:	
 208:	char *html_escape( char *str )
 209:	{
 210:	    int len ;
 211:	    char c, *tmp, *p, *res ;
 212:	
 213:	        len = strlen( str );
 214:	        tmp = malloc( len * 6 + 1 );
 215:	        if( tmp == 0 )
 216:	            return( 0 );
 217:	        p = tmp ;
 218:	        while( c = *str++ )
 219:	        {
 220:	            switch( c )
 221:	            {
 222:	            case '&': memcpy(p,"&amp;", 5); p += 5 ; break;
 223:	            case '<': memcpy(p,"&lt;",  4); p += 4 ; break;
 224:	            case '>': memcpy(p,"&gt;",  4); p += 4 ; break;
 225:	            case '"': memcpy(p,"&quot;",6); p += 6 ; break;
 226:	            default:  *p = c ;              p++ ;    break;
 227:	            }
 228:	        }
 229:	        *p = 0 ;
 230:	        res = strdup( tmp );
 231:	        free( tmp );
 232:	        return( res );
 233:	}
 234:	
safe_printenv() は、環境変数をgetenv() で検索し、その値を safe_print_string() で標準出力に出力している。

safe_print_string() は、次の html_escape() を使って文字列を安全なもの にして表示する。

html_escape() は、次の文字について、置き換えている。たとえば、 「<」は、「&lt;」と変換している。これで、 <SCRIPT> のような危険なスクリプトが送り込まれたとしても 「&lt;SCRIPT&gt;」と表示と表示されるだけで、スクリプトは実 行されない。

len * 6 は、最大で元の文字列の6倍の長さになることに備えている。

 235:	char *decode_hex( char *str )
 236:	{
 237:	    int len ;
 238:	    char c, *tmp, *p, *res ;
 239:	
 240:	        len = strlen( str );
 241:	        tmp = malloc( len + 1 );
 242:	        if( tmp == 0 )
 243:	            return( 0 );
 244:	        p = tmp ;
 245:	
 246:	        while( *str )
 247:	        {
 248:	            if( *str == '%' && isxdigit(*(str+1)) && isxdigit(*(str+2)) )
 249:	            {
 250:	                char hexstr[3] ;
 251:	                hexstr[0] = *(str+1);
 252:	                hexstr[1] = *(str+2);
 253:	                hexstr[2] = 0 ;
 254:	                c = strtol( content_length,0,10 );
 255:	                *p ++ = c ;
 256:	                str += 3 ;
 257:	            }
 258:	            else
 259:	            {
 260:	                *p ++ = *str ;
 261:	                str ++ ;
 262:	            }
 263:	        }
 264:	        *p = 0 ;
 265:	        res = strdup( tmp );
 266:	        free( tmp );
 267:	        return( res );
 268:	}
 269:	
 270:	int countchr( char *s, char c )
 271:	{
 272:	    int count ;
 273:	        for( count=0 ; *s ; s++ )
 274:	            if( *s == c )
 275:	                count ++ ;
 276:	        return( count );
 277:	}
decode_hex() は、「%hh(2桁の16進数)」を元のバイトに 戻すものである。isxdigit() は、16進数の文字として正しいかを調べてい る。strtol() で基底を 16 として変換している。

countchr() は、文字列の中でその文字がいくつ出てくるかを数えるものである。

実行例: http://www2.coins.tsukuba.ac.jp/~syspro/2005/No9_files/cgi-printarg.html

REQUEST_METHOD=GET
SCRIPT_NAME=/~syspro/2005/No9_files/cgi-printarg.cgi
QUERY_STRING=lastname=arg1&firstname=arg2&sex=Male&email=arg3
CONTENT_LENGTH=(null)
query_string:
lastname=arg1&firstname=arg2&sex=Male&email=arg3
qv[0]: lastname=arg1(lastname=arg1)
qv[1]: firstname=arg2(firstname=arg2)
qv[2]: sex=Male(sex=Male)
qv[3]: email=arg3(email=arg3)

REQUEST_METHOD=POST
SCRIPT_NAME=/~syspro/2005/No9_files/cgi-printarg.cgi
QUERY_STRING=
CONTENT_LENGTH=48
query_string:
lastname=arg1&firstname=arg2&sex=Male&email=arg3
qv[0]: lastname=arg1(lastname=arg1)
qv[1]: firstname=arg2(firstname=arg2)
qv[2]: sex=Male(sex=Male)
qv[3]: email=arg3(email=arg3)

アクセスログとエラーログ

WWW サーバは、ページがアクセスされる度に記録を残す。この記録をアクセス ログという。アクセスログは、情報学類のサーバ www (orchid-a) や www2 (orchid-e) では、次の場所にある。

/var/log/httpd/access_log

以下は、アクセスログの例である。


130.158.85.140 - - [18/Jun/2005:18:52:44 +0900]
"GET /~yas/coins/syspro-2005/No9.html HTTP/1.1" 200 57000
"-"
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312"

130.158.85.140 - - [18/Jun/2005:18:52:44 +0900] 
"GET /~yas/coins/syspro-2005/coins-syspro-2005.css HTTP/1.1" 200 1902
"http://www.coins.tsukuba.ac.jp/~yas/coins/syspro-2005/No9.html"
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312"

130.158.85.140 - - [18/Jun/2005:18:52:44 +0900] 
"GET /~yas/coins/syspro-2005/images/www-xss-normal.png HTTP/1.1" 404 334 
"http://www.coins.tsukuba.ac.jp/~yas/coins/syspro-2005/No9.html" 
"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312"
1行が1つのページの転送を意味する。左端は、クライアントの IP アドレス である。どのような要求が来たか、クライアントのソフトウェアなどがわかる。 自分が単にWWWブラウザを使っているだけでも、さまざまな情報をサーバに 送っているとに注意しなさい。

アクセスログの他に、エラーが起きた時にもログが作られる。これをエラーロ グという。エラーログは、情報学類のサーバ www (orchid-a) や www2 (orchid-e) では、次の場所にある。

/var/log/httpd/error_log

CGI のプログラムをデバッグする時には、このエラーログを見て原因を探る。

シェルからのCGIプログラムの実行

CGI のプログラムをシェルから実行してデバッグすることもできる。

GET の場合、環境変数 REQUEST_METHOD に GETと設定し、環境変数 QUERY_STRING にパラメタを設定する。

% setenv REQUEST_METHOD GET [←]
% setenv QUERY_STRING 'lastname=name1&firstname=name2&sex=Male&email=who@dom' [←]
% ./cgi-printarg.cgi [←]
Content-Type: text/html

<HTML><HEAD></HEAD><BODY><PRE>
REQUEST_METHOD=GET
SCRIPT_NAME=(null)
QUERY_STRING=lastname=name1&firstname=name2&sex=Male&email=who@dom
CONTENT_LENGTH=(null)
query_string:
lastname=name1&firstname=name2&sex=Male&email=who@dom
qv[0]: lastname=name1(lastname=name1)
qv[1]: firstname=name2(firstname=name2)
qv[2]: sex=Male(sex=Male)
qv[3]: email=who@dom(email=who@dom)
</PRE></BODY></HTML>
% []
POST の場合、まず、データをファイルに保存しておく。そのバイト数を wc コマンドなどで調べておく。環境変数 REQUEST_METHOD に POST と設定する。 環境変数 CONTENT_LENGTH にデータファイルの大きさを設定する(ここでは最 後の改行を取り除いたバイト数を指定している)。
% echo 'lastname=name1&firstname=name2&sex=Male&email=who@dom' > data [←]
% wc data [←]
      1       1      54 data
% setenv REQUEST_METHOD POST [←]
% unsetenv QUERY_STRING [←]
% setenv CONTENT_LENGTH 53 [←]
% ./cgi-printarg.cgi < data [←]
Content-Type: text/html

<HTML><HEAD></HEAD><BODY><PRE>
REQUEST_METHOD=POST
SCRIPT_NAME=(null)
QUERY_STRING=(null)
CONTENT_LENGTH=53
query_string:
lastname=name1&firstname=name2&sex=Male&email=who@dom
qv[0]: lastname=name1(lastname=name1)
qv[1]: firstname=name2(firstname=name2)
qv[2]: sex=Male(sex=Male)
qv[3]: email=who@dom(email=who@dom)
</PRE></BODY></HTML>
% []

CGI プログラムを実行するプロセスの権限

シェルからプロセスを生成しプログラムを実行する時には、その新しいプロセ スの権限(ユーザ属性、getuid() の結果)は、シェルのものからコピーされ る。すなわち、新しいプロセスはシェルと同じユーザの権限で動作する。CGI の場合、遠隔の WWW ブラウザのユーザは、その WWW サーバにログインする権 限はないが、CGI のプログラムを実行することができる。そのプログラムを、 どのユーザの権限で実行する方法としては、大きく次の2つ方法がある。 どちらの方式にも一長一短がある。 WWW サーバ・プロセスの権限で動作させた場合、

suEXEC を使う方法の場合、長所と短所は逆になる。

スクリプト言語の利用

CGI のプログラムを作成する場合、トップレベル(クライアントにデータを送 る部分)は、C言語で記述するよりも、スクリプト言語で記述することが多い。 スクリプト言語は、インタプリタで動作するので、プログラミングが容易である。

普通のプログラミング言語は、アプリケーション・プログラム本体を記述する 時に使われる。 これに対して、 スクリプト言語 は、アプリケーション本体で はなく、アプリケーションの細かな動作を変更したり、アプリケーション本体 を変更することなく機能を追加したりするために使われる言語である。 普通のプログラミング言語は、アプリケーション・プログラマにより使われ、 コンパイラで機械語に変換されるので、実行時には機械語しか残っていない。 これに対して、スクリプト言語は、アプリケーションのユーザや、システム管 理者などによって使われ、プログラムは、アプリケーションに組み込まれた インタプリタで解釈実行される。 スクリプト言語を使うと、単に変数を設定することに比べて、 高度な機能拡張が機能になる。

スクリプト言語の例:

スクリプト言語 csh で記述した CGI のプログラムの例:
% cat cgi-hello-csh.cgi [←]
#!/bin/csh -f

cat <<EOF
Content-Type: text/html

<HTML><HEAD></HEAD><BODY>
hello.
</BODY></HTML>
EOF
% ./cgi-hello-csh.cgi [←]
Content-Type: text/html

<HTML><HEAD></HEAD><BODY>
hello.
</BODY></HTML>
% []

SSI (Server Side Include)

Javaアプレット

JavaScript

JavaScript は、WWW ブラウザ上で動作するスクリプト言語である。

JavaScript は、文法が少し Java 言語に似ているが、Java とはまったく別の 言語である。WWW ページの中の Javaアプレットは、 「実行可能なインライン・イメージ」に似ている。これに対して JavaScript の記述は、「実行可能なインライン・テキスト」 に似ている。

JavaScrip のプログラムの例:

<SCRIPT LANGUAGE="JavaScript">
<!--
for( i=0 ; i<10; i++ )
  document.writeln("<P>hello,world</P>");
//-->
</SCRIPT>

これは、<P>hello,world</P> を 10 回書いた のと同じ効果がある。<!--//->は、JavaScriptを知らないブラウザにはコメント として扱われる。関数定義などは、ヘッダ部分 <HEAD></HEAD>に書くという方法もよく使われる。

JavaScript の言語としての特徴

JavaScript によるブラウザの制御

JavaScript の記述は、CGI と似ているところもあるが、JavaScript でないと できないものに、ブラウザの制御がある。たとえば、次の例では、ブラウザの (戻る)ボタンと同じ動きをさせることができる。

<A HREF="javascript:history.back();">戻る</A>

次の例は、<FORM></FORM>からパラメタを受け取るも のである。

<SCRIPT LANGUAGE="JavaScript">
function go(s,h,p)
{
	location.href = s.value + "://" + h.value + p.value ;
}
</SCRIPT>

<FORM NAME="form1">
<INPUT NAME="scheme" TYPE="text" VALUE="http"><BR>
<INPUT NAME="host"   TYPE="text"><BR>
<INPUT NAME="path"   TYPE="text" VALUE="/"><BR>
<INPUT TYPE="button" VALUE="go" onClick="go(form1.scheme,form1.host,form1.path)">
</FORM>

onClick 属性の値は、クリックした時に評 価される式であり、関数 go() が呼び出されている。引 数は、<FORM> の値である。関数 go() の中では、 .value フィールドから値が読み出されている。 location.href に代入することで そのページを表示させることができる。

この例では、<INPUT type="text"><INPUT type="button">が使われている。その他に、 <FORM>では、<INPUT TYPE="radio"><INPUT TYPE="checkbox"><SELECT>が使える。イベントとしては、 onClick が主に使われる。その他に、 onFocus が使われることもある。

JavaScript では、document.open() で新しく HTML の ドキュメントを生成して、それをブラウザに表示させることもできる。

クロスサイトスクリプティング攻撃

WWWブラウザでは、信頼しているサイトから送られてくるJavaScriptのプログ ラムだけを実行するようにし、攻撃サイトから送られてくるJavaScriptのプロ グラムを実行しないようにしたい。

WWW ブラウザ、攻撃サイト、信頼しているサイト、JavaScriptのプログラム

図? JavaScripの送信元サイトの区別

脆弱性があるサイトでは、攻撃サイトから送られてきた JavaScript のプログ ラムを中継してしまう。

WWW ブラウザ、攻撃サイト、脆弱性のあるサイト、JavaScriptのプログラム

図? JavaScripの送信元サイトの区別

これがクロスサイトスクリプティング攻撃(cross-site scripting atack, XSS atack)である。

CGI のプログラムをつくる時には、クロスサイトスクリプティング攻撃に気を つける。これは、クライアントから送られてる文字列の中に <SCRIPT>のようなタグが含まれていた場合、それをそのままクラ イアントに送り返すと問題がある。 (さらに、 %hh にも気をつける必要がある。)

クライアントから送られてきた文字列は、必ず検査し、安全な状態にして (sanitize)から使う。「<>&"」のようなタグが含まれている場合 には注意する。このような文字列を受け取った場合、不用意に送り返してはい けない。送り返す時には、html_escape() のよう な方法で必ずエスケープする。

他のプログラムの実行

CGI では、意図していないプログラムを実行しないようにする。

他のプログラムの実行(C言語)

他のプログラムを実行する時には、execve() のようなシステムコールを使い、 かつ、限られたプログラムしか実行しないようにすると安全性が高くなる。ク ライアントから送られてきた文字列をsystem() や popen() に渡してプログラ ムを実行する時には、必ず検査する。特にシェルが解釈する特殊な文字 「| & ; && || `」などが含 まれていた場合、意図しないプログラムが実行されることがある。

char *user ;
...
snprintf(cmd,BUFSIZE,"finger %s",user );
f = poepn(cmd,"r");
もし、user に ";""|" が含まれていたら、、、 
f = poepn("finger yas; /bin/sh","r");

他のプログラムの実行(Perl言語)

Perl には、バッファ・オーバーフローの問題はない。

perl の open() には、危険性がある。 C 言語のライブラリ関数 popen() と同じ動きをすることがある。

open(FILE, "|cmd")
Perl の危険な関数、式、

参考

産業技術総合研究所 グリッド研究センター セキュアプログラミングチーム
http://securit.gtrc.aist.go.jp/
)

IPA セキュア・プログラミング講座
http://www.ipa.go.jp/security/awareness/vendor/programming/
)

CGIでのその他の注意事項

ファイルのロック

アクセスカウンタなどデータ・ファイルを書き換えるような CGI プログラム の場合、複数のプログラムが同時に実行された時のことを考慮する必要がある。 必要に応じて、ファイルのロックを行うなどして、一度に1つのプロセスしか プログラムを実行しないようにする。このことを、相互排除という。

相互排除について詳しくは、後半のマルチスレッド・プログラミングや 2学期のオペレーティング・システムで扱う。 特に、扱うべき資源が複数になった場合には、デッドロックに気をつける。

Unix では、ファイルに対するロックを扱うために、次のような機能を提供し ている。

flock() システムコール
ファイル単位のロック
fcntl() システムコールと F_GETLK, F_SETLK, F_SETLKW
レコード単位のロック
lockf() ライブラリ関数
レコード単位のロック
CGI のプログラムでなくても、ロックをしなければならないことがある。

セッション管理

CGI は、基本的には1回の操作で完結する。買い物かごを作るには、複数の CGI のプログラムの間で一連の操作を1つの連続したもの(セッション)とし て扱う必要がある。

セッション管理には、次のような方法がある。

セッション管理にクッキーを使う方法は、正しく使えば比較的安全である。し かし、クッキーはセッションを越えて有効なもの、異なるサイトに送られるも のも定義できる。また、クッキーにパスワードを埋め込むという幼稚で危険な 実装も時々見受けられる。

セッションは、認証とは別である。同じ人が複数のセッションを実行すること もある。

クッキー

cookieは、コンピュータ・サイエンスの専門用語。

協調して動作しているプログラムの間で、ある一連の作業を識別するための数 を意味する。

RPC、HTTP(WWW)で使われる。

RPCでのクッキー

3学期の「 分散システム」で扱う。

WWWでのcookie

アクセス制御

WWWは、もともと大勢の人にメッセージを伝えるための仕組である。 あるページは、ある特定の人(同じ研究室・部署の人)だけに しか見えないように設定することもできる。

アクセス制御 とは、「主体」が、「オブジェクト」を「操作」する時、どんな操作なら正し いということを定義して、それがきちんと守られていることをということを保 証することである。

WWW ページのアクセス制御は、主体は、WWW ブラウザ(を使っている人)、 オブジェクトは、サーバ側にあり、URL で示されるもの(ファイル、ディレクトリ、CGI のプログラム)、 操作としては、GET、POST などがある。

アクセス制御の方針(ポリシー)は、サーバ側で既述される。 この時、次のような情報がよく使われる。

アクセス制御は、基本的には、WWWサーバの管理者の仕事である。 WWWサーバ (httpd) の設定ファイル ( /usr/local/etc/httpd/conf/httpd.conf, /var/www/conf/httpd.conf など ) に記述される。

個人のページについては、 .htaccess というファイルを作れば、個人で変更できるようになっている場合がある。

IPアドレスによるアクセス制御

IPアドレスにより筑波大学の学内にだけアクセス可能なページを作るための .htaccess の例。
order deny,allow
deny from all
allow from 130.158.0.0/16 133.51.0.0/16 192.50.17.0/24

パスワードが設定されたページのアクセス

例:パスワード・ファイル /home/lab/Denjo/yas/etc/passwd-doc1 に登録されているユーザだけが、 .htaccess があるディレクトリ以下にあるファイルをアクセスできる。

AuthType Basic
AuthName "Members Only"
AuthUserFile /home/lab/Denjo/yas/etc/passwd-doc1
require valid-user
このパスワード・ファイルは、サーバ(www,www2)上で htpasswd というプログ ラムで作る。
% ls /home/lab/Denjo/yas/etc/passwd-doc1 [←]
ls: /home/lab/Denjo/yas/etc/passwd-doc1: No such file or directory
% htpasswd -c /home/lab/Denjo/yas/etc/passwd-doc1 user1 [←]
Adding password for user1.
New password:user1のパスワードを打ち込む[←]
Re-type new password:user1のパスワードを打ち込む[←]
% htpasswd /home/lab/Denjo/yas/etc/passwd-doc1 user2 [←]
Adding user user2
New password:user2のパスワードを打ち込む[←]
Re-type new password:user2のパスワードを打ち込む[←]
% cat /home/lab/Denjo/yas/etc/passwd-doc1  [←]
user1:1fjr1tHIgoG7U
user2:qXaeA9Zge7Yqc
% []
一番最初は、-c オプション付で実行する。passwd コマ ンドと同様に、打ち込んだパスワードは、画面には表示されず、また、確 認のために2回打つ必要がある。

htpasswd コマンドの結果、次のようなファイルが作られる。

user1:1fjr1tHIgoG7U
user2:qXaeA9Zge7Yqc
ユーザ名と暗号化されたパスワード(ハッシュ値)から構成される。 このファイルは、~/public_html 以外の場所に置くと GET で盗まれることはない。

このアクセス制御が有効なファイルをクライアントが GET したとする。

GET /dir1/file1.html HTTP/1.0[←]
[←]
すると、サーバから次のようなエラーが返される。
HTTP/1.1 401 Authorization Required
WWW-Authenticate: Basic realm="Members Only"
Content-Type: text/html

<HTML><HEAD>
<TITLE>401 Authorization Required</TITLE>
</HEAD><BODY>
...
</BODY></HTML>
.htaccessAuthTypeAuthName の内容が、HTTP の応答の WWW-Authenticate: に現れる。

このページをアクセスするには、クライアント(WWWブラウザ)は、ウインドウ を開いてユーザにユーザ名とパスワードを要求する。そして、次のような GET 要求をもう一度送る。

GET /dir1/file1.html HTTP/1.0[←]
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==[←]
[←]
Authorization: には、ユーザが打ち込んだユーザ名とパスワードが Base64 (64進数)で符合化されて含まれている。 これを受け取ったサーバは、 AuthUserFile で指定されたファイルを開いて、ユーザ名とパスワー ドパスワードを照合して、正しければアクセスを許する。

Base64 は、「暗号ではない」ので、簡単に元にもどせる。

% echo QWxhZGRpbjpvcGVuIHNlc2FtZQ== | openssl base64 -d [←]
Aladdin:open sesame% []
AuthType Basic を使うアクセス制御の方法では、パスワードがそのままネッ トワークを流れてしまうので、盗聴に弱い。

練習問題と課題

練習問題(83) WWW サーバ上のデータの観察

htttp://www.coins.tsukuba.ac.jp/ のページは、サーバ (www) 上の /var/www/htdocs/ 以下にある。

WWW ブラウザや telnet を使って得た結果と /var/www/htdocs/ 以下のファイ ルが同一であることをwww に ssh でログインして確認しなさい。

同様に www2 についても調べなさい。

練習問題(84) アクセスログの観察

アクセスログは、サーバ(www,www2)上の /var/log/httpd/access_log というファイルにためられる。 これを、次のようにして観察しなさい。
  1. ssh で www または www2 にログインする。
  2. 次のように tail コマンドを実行する。tail コマンドは、 -f オプションを付けて実行すると終了しないので、 最後に ^C キーで終了させる。
    
    % tail -f /var/log/httpd/access_log [←]
    
  3. WWW ブラウザ(Netscape など)を実行して、 http://www.coins.tsukuba.ac.jp/や自分の WWW ページを開く。
  4. 「再読込み」を行う。
  5. データが流れていく時には、適当な所で ^C で tail コマンドを終了させる。
  6. 自分が使っているコンピュータのホスト名が表示されるかを見る。端末 のスクロール機能を使う。
同様に、/var/log/httpd/error_log についても調べなさい。

練習問題(85) 引数の表示

次の CGI プログラムに対して、フォームを定義し、何かデータを送ってみなさい。

http://www2.coins.tsukuba.ac.jp/~syspro/2005/No9_files/cgi-printarg.cgi

練習問題(86) CGIによる足算

2つのデータを受け取り、その結果を加えた数を表示するようなCGI のプログ ラムを作成しなさい。

ヒント:

  1. "arg1=10&arg2=20"のように送るようなフォームを想定 してプログラムを作成する。
  2. cgi-printarg.cで、余計な表示をなくす。
  3. make_qcqv() で分解した後、"arg1=" や "arg2=" があるパラメタを検索 する。これには、まず次のような関数 getparam() を定義するとよい。
    char *getparam( int qc, char **qv, char *name )
    {
    	qv[i] の中で "変数名=値" となっているものを探す。
    	見つければ "値" の部分を返す。
    	見つからなければ、NULL を返す。
    }
    
  4. 得られた文字列を、strtol() 等で整数に変換し、足算を行う。結果を printf() の %d などで表示する。

  5. 作成したプログラムを、シェルから実行 して動作を確認する。
  6. "arg1=10&arg2=20"のように送るようなフォームを作成する。 フォームのデバッグには、cgi-printarg.cgiをそのま ま使ってきちんと表示されるかどうかを調べるとよい。

余裕があれば、メニューやラジオボタンなどで足算以外の計算もできるように しなさい。

練習問題(87) カレンダの表示

fork() と execve() を使って、/usr/bin/cal を実行し、WWW ブラウザに今月 のカレンダを実行するプログラムを作りなさい。cal コマンドは、引数を付け ずに実行すると、今月のカレンダーを表示する。
% cal [←]
      June 2005
Su Mo Tu We Th Fr Sa 
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30

% []

CGI プログラムを実行した月のカレンダーを表示しなさい。固定した表示する だけなら、CGI の意味はない。余計な表示は省略すること。

余裕があれば、今月のカレンダーではなく、任意の年・月のカレンダーを表示 しなさい。

この課題では、system() や popen() を用いてはならない。この課題に限らず、 もし、遠隔から任意のコマンドを実行可能なような CGI プログラムを作成し た時には、単位を出さない。

練習問題(88) CGIプログラムの実行権限

CGI プログラムが、どのユーザの権限で動作しているかを調べるプログラムをつくりなさい。

ヒント:CGI のプログラムから id コマンドを実行させる。

練習問題(89) IPアドレスによるアクセス制御

特定のホスト(たとえば、adonis10)からだけしかアクセスできないような .htaccess を定義しなさい。

練習問題(90) パスワードによるアクセス制御

ある特定のユーザ名とパスワードを知っている人だけからしか アクセスできないような .htaccess を定義しなさい。 そして、htpasswd コマンドを使って、パスワードを設定しなさい。

注意:ユーザ名は、長くてもよい。Unix や Windows のユーザ名とは別のもの でよい。

注意:ここで設定したパスワードは、ネットワーク上を暗号化されずに流れる。 www2 で実験する時には、決して Unix や Windows のログインのパスワードと 同じものを使ってはならない。

練習問題(91) クロスサイトスクリプティング攻撃に対する脆弱性の除去

今まで「自分で」作成した CGI のプログラムの中で、 クロスサイトスクリプティング攻撃に対する脆弱性 あるものを探しなさい。そして、それを修正しなさい。

レポートには、修正個所が簡単にわかるような説明をつけなさい。

練習問題(92) 他のプログラムの実行を許してしまうような脆弱性の除去

今まで「自分で」作成した CGI のプログラムの中で、 他のプログラムの実行を許してしまうような脆弱性が あるものを探しなさい。そして、それを修正しなさい。

レポートには、修正個所が簡単にわかるような説明をつけなさい。


Last updated: 2005/06/25 00:49:04
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>