土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

CodeQL(静的解析ツール)の挙動を確認するための対象ソースコードとしてTinyhttpdを調査する

前回 は、VSCode(Visual Studio Code)で、CodeQL を使う方法について調べました。

CodeQL の挙動を理解するには、対象のソースコードを少し変更して、CodeQL の結果を確認する、を繰り返すことが必要そうです。しかし、これまで対象のソースコードとして、使いたいと考えていた Use-After-Free は、1関数しかなく、データフロー解析を試すことを考えると、少し使いにくいと感じました。そこで、今回は、GitHub に公開されている OSS のうち、軽めの HTTPサーバの実装である Tinyhttpd を調査します。これが CodeQL の対象ソースコードとして、使いやすいといいのですが、まだ分かりません。

それでは、やっていきます。

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方(GPU実行時間の見積りとパスワード付きZIPファイル)
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った
・第34回:TryHackMeを始めてみたけどハードルが高かった話
・第35回:picoCTFを始めてみた(Beginner picoMini 2022:全13問完了)
・第36回:picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります)
・第37回:picoCTF 2024:Reverse Engineeringの全7問をやってみた(Windowsプログラムの3問は後日やります)
・第38回:picoCTF 2024:General Skillsの全10問をやってみた
・第39回:picoCTF 2024:Web Exploitationの全6問をやってみた(最後の2問は解けず)
・第40回:picoCTF 2024:Forensicsの全8問をやってみた(最後の2問は解けず)
・第41回:picoCTF 2024:Cryptographyの全5問をやってみた(最後の2問は手つかず)
・第42回:picoCTF 2023:General Skillsの全6問をやってみた
・第43回:picoCTF 2023:Reverse Engineeringの全9問をやってみた
・第44回:picoCTF 2023:Binary Exploitationの全7問をやってみた(最後の1問は後日やります)
・第45回:書籍「セキュリティコンテストのためのCTF問題集」を読んだ
・第46回:書籍「詳解セキュリティコンテスト」のReversingを読んだ
・第47回:書籍「詳解セキュリティコンテスト」のPwnableのシェルコードを読んだ
・第48回:書籍「バイナリファイル解析 実践ガイド」を読んだ
・第49回:書籍「詳解セキュリティコンテスト」Pwnableのスタックベースエクスプロイトを読んだ
・第50回:書籍「詳解セキュリティコンテスト」Pwnableの共有ライブラリと関数呼び出しを読んだ
・第51回:picoCTF 2025:General Skillsの全5問をやってみた
・第52回:picoCTF 2025:Reverse Engineeringの全7問をやってみた
・第53回:picoCTF 2025:Binary Exploitationの全6問をやってみた
・第54回:書籍「詳解セキュリティコンテスト」Pwnableの仕様に起因する脆弱性を読んだ
・第55回:システムにインストールされたものと異なるバージョンのglibcを使う方法
・第56回:書籍「詳解セキュリティコンテスト」Pwnableのヒープベースエクスプロイトを読んだ
・第57回:書籍「解題pwnable」の第1章「準備」を読んだ
・第58回:書籍「解題pwnable」の第2章「login1(スタックバッファオーバーフロー1)」を読んだ
・第59回:書籍「解題pwnable」の第3章「login2(スタックバッファオーバーフロー2)」を読んだ
・第60回:書籍「解題pwnable」の第4章「login3(スタックバッファオーバーフロー3)」を読んだ
・第61回:書籍「解題pwnable」の第5章「rot13(書式文字列攻撃)」を読んだ
・第62回:GitHubが開発した静的解析ツール(脆弱性検出ツール)のCodeQLを使ってみる
・第63回:CodeQL(静的解析ツール)で使われるクエリの選ばれ方を調べた
・第64回:CodeQL(静的解析ツール)のクエリの書き方を調べた
・第65回:CodeQL(静的解析ツール)で使われているアラートクエリの中身を調べる
・第66回:CodeQL(静的解析ツール)で使われているパスクエリの中身を調べる
・第67回:CodeQL(静的解析ツール)をVSCodeで使う方法を理解する
・第68回:CodeQL(静的解析ツール)の挙動を確認するための対象ソースコードとしてTinyhttpdを調査する ← 今回

以下は、Tinyhttpd の GitHub です。

github.com

CodeQL を実行するためのソースコードとして、簡単すぎず、難しすぎず、かつ、外部から入力を受けるようなものを探しました。ChatGPT にいろいろ相談したところ、このソースコードを推薦されました。

なお、現在の実行環境は、Windows11 に、WSL2 で、Ubuntu 24.04 を入れた環境です。

Tinyhttpdの概要

Tinyhttpd は、シンプルなマルチスレッド対応の簡易 HTTPサーバで、GET/POST を受け付け、静的ファイル配信と CGI実行を行うことができます。

Tinyhttpdの動作を確認する

まずは、GitHub からソースコードを取得して、動作確認していきます。

クローンとビルドを行います。ビルドでは、いくつか警告が出ますが、まずは気にしないことにします。また、ビルドにより、Tinyhttpd の本体である httpd という実行ファイルと、簡易クライアント?の client という実行ファイルが生成されます。

$ git clone https://github.com/EZLippi/Tinyhttpd.git

$ cd Tinyhttpd/

$ make

では、実際に動かしていきます。

実行すると、4000番ポートでクライアントからのアクセスを待っている状態になります。

$ ./httpd
httpd running on port 4000

ここで、ビルドで生成されたクライアントを動かしてみます。

エラーになりました。

$ ./client
oops: client1: Connection refused

ソースコードの simpleclient.c を見てみます。

ポート番号が、9734 となっており、サーバの設定(4000)と異なっていました。4000番ポートに修正して再ビルドします。

再度実行してみます。

何も反応がありません。

$ ./client
^C

仕方ないので、VSCode で、サーバ側をデバッガで動かして、動作を確認しました。

クライアントは、1文字だけ、'A' を送っていて、それは受信できていました。その後、サーバは、次の文字を待ち続けていました。

クライアントを終了すると、サーバの recv関数から応答が返り、何も受信しなかったことが分かります。

このクライアントソフトは、何を目的としているのか分かりません。おそらくデバッグ用途だと思います(まともな動作は、そもそも期待してない)。

では、このクライアントソフトで動作確認するのは諦めて、普通のブラウザを使って確認したいと思います。

ブラウザを起動して、http://localhost:4000/ にアクセスします。すると、以下の画面が表示されました。ここまでは、正しく動作してるようです。

ブラウザの表示画面
ブラウザの表示画面

入力フォームに red と入力して、送信ボタンをクリックしてみます。

redを入力した後のブラウザの表示画面
redを入力した後のブラウザの表示画面

うまく動いていないようです。

いくつか修正しました。

まずは、htdocs/color.cgi と、htdocs/check.cgi に実行権限がなかったので、以下を実行しました。

$ chmod +x htdocs/check.cgi

$ chmod +x htdocs/color.cgi

htdocs/color.cgi と、htdocs/check.cgi の内容を見ると、シェバン行が #!/usr/local/bin/perl -Tw となってますが、私の環境では、perl は、/usr/bin/perl だったので、パスを修正しました。

まだ動かないので、CGIファイルを直接実行してみました。

ChatGPT に原因を聞いてみると、CGIモジュールがインストールされてないということでした。

$ REQUEST_METHOD=GET QUERY_STRING=color=red htdocs/color.cgi
Can't locate CGI.pm in @INC (you may need to install the CGI module) (@INC entries checked: /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.38.2 /usr/local/share/perl/5.38.2 /usr/lib/x86_64-linux-gnu/perl5/5.38 /usr/share/perl5 /usr/lib/x86_64-linux-gnu/perl-base /usr/lib/x86_64-linux-gnu/perl/5.38 /usr/share/perl/5.38 /usr/local/lib/site_perl) at htdocs/color.cgi line 4.
BEGIN failed--compilation aborted at htdocs/color.cgi line 4.

CGIモジュールをインストールします。

$ sudo apt install libcgi-pm-perl

もう一度、CGIファイルを実行してみます。

ちゃんと HTML が返ってきてるので、うまくいってるようです。

$ REQUEST_METHOD=GET QUERY_STRING=color=red htdocs/color.cgi
Content-Type: text/html; charset=ISO-8859-1

<!DOCTYPE html
        PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
<head>
<title>RED</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
</head>
<body bgcolor="red">
<h1>This is red</h1>
</body>
</html>

では、httpd を実行してみます。うまく動きました。

redを入力した後のブラウザの表示画面(2回目)
redを入力した後のブラウザの表示画面(2回目)

Tinyhttpdの詳細

ここからは、Tinyhttpd のソースコードを見ていきます。

main関数

まずは、main関数です。

コメントで、add と書いたところと、debug と書いたところは、私が追加したところです。add の方は、通常は初期化されるところなので、入れておきました。debug の方は、クライアントが接続してきたときに、クライアントの情報を表示しています。

startup関数は後述しますが、TCPサーバとしてのソケットの初期化を行っています。その後、while の無限ループに入ります。

ループ内では、accept関数で、クライアントからの接続を受け付け、クライアントごとに、 pthread_create関数で、スレッドを作成して、そのスレッドにクライアントの処理を任せています。

int main(void)
{
    int server_sock = -1;
    u_short port = 4000;
    int client_sock = -1;
    struct sockaddr_in client_name;
    socklen_t  client_name_len = sizeof(client_name);
    pthread_t newthread;

    server_sock = startup(&port);
    printf("httpd running on port %d\n", port);

    memset(&client_name, 0, sizeof(client_name));// add

    while (1)
    {
        client_sock = accept(server_sock,
                (struct sockaddr *)&client_name,
                &client_name_len);
        if (client_sock == -1)
            error_die("accept");
        {
            // debug
            char buf[INET_ADDRSTRLEN] = {};
            inet_ntop(AF_INET, &client_name.sin_addr, buf, sizeof(buf));
            printf("accept from %s:%u\n", buf, ntohs(client_name.sin_port));
            fflush(stdout);
        }
        /* accept_request(&client_sock); */
        if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
            perror("pthread_create");
    }

    close(server_sock);

    return(0);
}

startup関数

次は、startup関数です。

普通の TCPサーバの実装です。上位関数からポート番号 0 で指定された場合は、動的にポートを割り当てています。

listen関数で、クライアントを 5つまで受け付けています。

int startup(u_short *port)
{
    int httpd = 0;
    int on = 1;
    struct sockaddr_in name;

    httpd = socket(PF_INET, SOCK_STREAM, 0);
    if (httpd == -1)
        error_die("socket");
    memset(&name, 0, sizeof(name));
    name.sin_family = AF_INET;
    name.sin_port = htons(*port);
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)  
    {  
        error_die("setsockopt failed");
    }
    if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
        error_die("bind");
    if (*port == 0)  /* if dynamically allocating a port */
    {
        socklen_t namelen = sizeof(name);
        if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
            error_die("getsockname");
        *port = ntohs(name.sin_port);
    }
    if (listen(httpd, 5) < 0)
        error_die("listen");
    return(httpd);
}

accept_request関数

accept_request関数は、先ほど、pthread_create関数で作成したスレッド関数です。私の方で、いくつかコメントを付けています。

get_line関数で、クライアントから受信した 1行を取得しています(numchars は取得した文字数)。その後の printf関数は私が追加しました。

最初の while文は、スペースが見つかるまで、文字列を取得しています。先頭は、GET か POST を期待しています。いずれにもマッチしない場合は、エラーを返す unimplemented関数を実行しています。

GET or POST で、POST なら cgiフラグをセットしておきます。その後、空白を読み飛ばした後、URL を読み出します。

次は、GET の場合の処理です。先ほど取得した URL を解析します。URL に ? が含まれている場合は、?\0 に変更して、query_stringポインタに、? の次の位置に設定して、cgiフラグをセットしておきます。

sprintf関数で、要求されたパスを、htdocsディレクトリ内のパスとして、path変数に格納しています。末尾が / の場合、index.html の文字列を追加しておきます。

stat関数で、要求されたパスのファイルが存在しない場合、受信データを全て読み出した後、エラーを表示する not_found関数を実行します。

ファイルが存在した場合、かつ、ディレクトリだった場合は、index.html の文字列を追加します。そのファイルが実行可能なファイルだったら、cgiフラグをセットします。

最後に、cgiフラグがセットされていたら、execute_cgi関数を実行し、そうでなければ、serve_file関数を実行します。

void accept_request(void *arg)
{
    int client = (intptr_t)arg;
    char buf[1024];
    size_t numchars;
    char method[255];
    char url[255];
    char path[512];
    size_t i, j;
    struct stat st;
    int cgi = 0;      /* becomes true if server decides this is a CGI
                       * program */
    char *query_string = NULL;

    numchars = get_line(client, buf, sizeof(buf));
    printf("get_line(): numchars=%ld\n", numchars);
    i = 0; j = 0;
    while (!ISspace(buf[i]) && (i < sizeof(method) - 1))// 先頭は GET or POST のはず
    {
        method[i] = buf[i];
        i++;
    }
    j=i;
    method[i] = '\0';

    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))// いずれにも一致しなければエラーを返す
    {
        unimplemented(client);
        return;
    }

    if (strcasecmp(method, "POST") == 0)
        cgi = 1;

    i = 0;
    while (ISspace(buf[j]) && (j < numchars))// 空白を読み飛ばす
        j++;
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))// URL読み込み
    {
        url[i] = buf[j];
        i++; j++;
    }
    url[i] = '\0';

    if (strcasecmp(method, "GET") == 0)
    {
        query_string = url;
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        if (*query_string == '?')
        {
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }

    sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");
    if (stat(path, &st) == -1) {
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        not_found(client);
    }
    else
    {
        if ((st.st_mode & S_IFMT) == S_IFDIR)
            strcat(path, "/index.html");
        if ((st.st_mode & S_IXUSR) ||
                (st.st_mode & S_IXGRP) ||
                (st.st_mode & S_IXOTH)    )
            cgi = 1;
        if (!cgi)
            serve_file(client, path);
        else
            execute_cgi(client, path, method, query_string);
    }

    close(client);
}

get_line関数

get_line関数は、クライアントから受信した 1行を上位に返します。

通常の HTTP のデータは、改行コードが、\r\n ですが、この関数は、\r や、\n にも対応しています。どの改行コードを受信したとしても、buf に格納するのは \n です。

途中のところは、コメントで説明を付けておきました。末尾に、\0 を入れて、この関数は終了します。

int get_line(int sock, char *buf, int size)
{
    int i = 0;
    char c = '\0';
    int n;

    while ((i < size - 1) && (c != '\n'))// -1 は、末尾に \0 を入れるため
    {
        n = recv(sock, &c, 1, 0);// 第4引数はflags
        /* DEBUG printf("%02X\n", c); */
        if (n > 0)
        {
            if (c == '\r')// \n、\r\n、\r いずれの場合も \n とする
            {
                n = recv(sock, &c, 1, MSG_PEEK);// MSG_PEEK:読まずに覗く
                /* DEBUG printf("%02X\n", c); */
                if ((n > 0) && (c == '\n'))
                    recv(sock, &c, 1, 0);// \r\nの場合
                else
                    c = '\n';// \rの場合
            }
            buf[i] = c;
            i++;
        }
        else
            c = '\n';
    }
    buf[i] = '\0';

    return(i);// \n を含む文字数
}

serve_file関数

accept_request関数から呼ばれている serve_file関数です。CGIファイルではない、通常の HTMLファイルを表示します。

whileループ前に、buf に設定しているのは、while の条件を満たすためだと思います。do while を使えばいいだけのような気がします。リクエストの残りを全て読むために、読み飛ばしています。

上位から与えられたファイルパスに対して、ファイルオープンして、headers関数と、cat関数を呼び出しています。

void serve_file(int client, const char *filename)
{
    FILE *resource = NULL;
    int numchars = 1;
    char buf[1024];

    buf[0] = 'A'; buf[1] = '\0';
    while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
        numchars = get_line(client, buf, sizeof(buf));

    resource = fopen(filename, "r");
    if (resource == NULL)
        not_found(client);
    else
    {
        headers(client, filename);
        cat(client, resource);
    }
    fclose(resource);
}

headers関数

serve_file関数から最初に呼び出されている headers関数です。

HTTP のレスポンスのヘッダ部分を送信しています。ヘッダの末尾には、\r\n だけの行が必要だったと思いますので、そこもちゃんと送信しています。

void headers(int client, const char *filename)
{
    char buf[1024];
    (void)filename;  /* could use filename to determine file type */

    strcpy(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
}

cat関数

serve_file関数から呼び出されている、もう1つの関数の cat関数です。

HTTP のレスポンスのボディ部分を送信しています。こちらも、do while が好きじゃないのか、fgets関数が、2回登場します。

ファイルの内容をそのまま出力しているだけですね。

void cat(int client, FILE *resource)
{
    char buf[1024];

    fgets(buf, sizeof(buf), resource);
    while (!feof(resource))
    {
        send(client, buf, strlen(buf), 0);
        fgets(buf, sizeof(buf), resource);
    }
}

execute_cgi関数

accept_request関数から呼ばれている、もう 1つの関数の serve_file関数です。CGIプログラムを実行します。

これまでで一番難しい関数ですね。

最初は、GET と POST で分岐します。GET の方は、リクエストボディが存在しないので、残りのリクエストを読み飛ばします。POST の方は、Content-Length を見つけたときは、その値を読み取り、\n が出現した時点でループを抜けます(受信データには、リクエストボディが残っている)。

次に、この後、fork で、子プロセスを生成しますが、親子のプロセス間で通信するために、ここで、パイプを 2つ生成します。子プロセスは実際に CGIファイルを実行します。cgi_output は、子プロセスの出力用(親プロセスは入力となる)で、cgi_input は、子プロセスの入力用(親プロセスは出力となる)です。パイプは読み書き両用はできないので、読み込み用、書き込み用の 2つを用意する必要があります。

fork関数を実行すると、子プロセスが生成されます。親プロセス、子プロセス、ともに、fork関数の呼び出しから戻ります。親プロセス側は、fork関数の戻り値として、子プロセスのプロセスID が返って、子プロセス側は、fork関数の戻り値は 0 になります。これにより、同じソースコードでも、親プロセスと子プロセスで処理を分けることができます。

その後、両方のプロセスが、HTTPリクエストの応答を返しています。うーん、この場合は親プロセスだけが応答を返せばいいと思うのですが、、、ちょっと理由が分かりません。

まず、pid が 0 の場合の子プロセス側の処理から見ていきます。

まず、dup2関数で、子プロセスの書き込み用のパイプを、標準出力として複製します。これにより、子プロセスが標準出力に出力する(printf関数などを使って)と、親プロセスとのパイプにも書き込まれることになります。さらに、dup2関数を実行して、子プロセスの読み取り用のパイプを標準入力として複製します。その後、使用しない方のパイプ(親プロセスが使用する)をクローズしておきます。

次は、putenv関数を使って、環境変数の設定を行います。REQUEST_METHOD に GET or POST を設定します。その後、GET の場合は、QUERY_STRING に ? 以降の内容を設定します。POST の場合は、CONTENT_LENGTH に先ほど取得した内容を設定します。

最後に、execl関数を使って、CGIファイルを実行します。

次に、親プロセス側です。最初に、子プロセス側と同様に、使わないパイプをクローズします。

POST の場合、リクエストボディを Content-Length の分だけ読み出し、子プロセスに送信します。あとは、子プロセスから書き込みがあると、その内容をクライアントに送信するループを実行します。CGIプログラムが終了すると、ループを抜けて、パイプをクローズして、子プロセスの終了ステータスを読み出して終了します。

void execute_cgi(int client, const char *path,
        const char *method, const char *query_string)
{
    char buf[1024];
    int cgi_output[2];
    int cgi_input[2];
    pid_t pid;
    int status;
    int i;
    char c;
    int numchars = 1;
    int content_length = -1;

    buf[0] = 'A'; buf[1] = '\0';
    if (strcasecmp(method, "GET") == 0)
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
    else if (strcasecmp(method, "POST") == 0) /*POST*/
    {
        numchars = get_line(client, buf, sizeof(buf));
        while ((numchars > 0) && strcmp("\n", buf))
        {
            buf[15] = '\0';
            if (strcasecmp(buf, "Content-Length:") == 0)
                content_length = atoi(&(buf[16]));
            numchars = get_line(client, buf, sizeof(buf));
        }
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }
    else/*HEAD or other*/
    {
    }

    if (pipe(cgi_output) < 0) {
        cannot_execute(client);
        return;
    }
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);
        return;
    }

    if ( (pid = fork()) < 0 ) {
        cannot_execute(client);
        return;
    }
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    if (pid == 0)  /* child: CGI script */
    {
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        dup2(cgi_output[1], STDOUT);
        dup2(cgi_input[0], STDIN);
        close(cgi_output[0]);
        close(cgi_input[1]);
        sprintf(meth_env, "REQUEST_METHOD=%s", method);
        putenv(meth_env);
        if (strcasecmp(method, "GET") == 0) {
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            putenv(query_env);
        }
        else {   /* POST */
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            putenv(length_env);
        }
        execl(path, NULL);
        exit(0);
    } else {    /* parent */
        close(cgi_output[1]);
        close(cgi_input[0]);
        if (strcasecmp(method, "POST") == 0)
            for (i = 0; i < content_length; i++) {
                recv(client, &c, 1, 0);
                write(cgi_input[1], &c, 1);
            }
        while (read(cgi_output[0], &c, 1) > 0)
            send(client, &c, 1, 0);

        close(cgi_output[0]);
        close(cgi_input[1]);
        waitpid(pid, &status, 0);
    }
}

htdocs/index.htmlファイル

トップページの index.htmlファイルです。

color.cgi が使われています。

<HTML>
<TITLE>Index</TITLE>
<BODY>
<P>Welcome to J. David's webserver.
<H1>CGI demo
<FORM ACTION="color.cgi" METHOD="POST">
Enter a color: <INPUT TYPE="text" NAME="color">
<INPUT TYPE="submit">
</FORM>
</BODY>
</HTML>

htdocs/color.cgi

index.html で実行されている color.cgi です。

perl は、かなり前にやってましたが、すっかり忘れたので、ChatGPT に解説してもらいました。

シェバンのオプションの T は、taint(汚染)チェックを有効化している、とのことで、セキュリティ強化の意味だそうです。もう1つのオプションの w は、警告を有効化しているそうです。

use strict は、文法の厳格化で、use CGI は、CGIモジュールの読み込みだそうです。

あとは、コメントに書きました。

#!/usr/bin/perl -Tw

use strict;
use CGI;

my($cgi) = new CGI; # CGIオブジェクトの生成

print $cgi->header; # Content-Type: text/html などの HTTPヘッダ出力
my($color) = "blue"; # 初期値設定
$color = $cgi->param('color') if defined $cgi->param('color'); # colorパラメータがあれば上書き

print $cgi->start_html(-title => uc($color),
                       -BGCOLOR => $color); # <html><head><title>…</title><body> を生成、タイトルと背景色を $color に設定
print $cgi->h1("This is $color"); # h1タグの出力
print $cgi->end_html; # </body></html> を出力

おわりに

今回は、GitHub に公開されている OSS のうち、軽めの HTTPサーバの実装である Tinyhttpd を調査しました。実際に動かすまでに、いろいろと対応する必要があったので、少し時間がかかりました。また、関数の説明については、主要なところは書きましたが、必要に応じて追加しようと思います。

次回は、Tinyhttpd に手を加えて、CodeQL の結果がどう変わっていくのかを調査したいと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。