Gawk (GNU awk) のパフォーマンス

以前にGNU grepのパフォーマンス向上についてお話しましたが、実はGawkのマッチング部分にはGNU grepと同じコードが使われています。ということは、Gawkのパフォーマンスも向上しているのでしょうか?

Gawk 3.1.7

まずは、Red Hat Enterprise Linux 6にバンドルされているGawk 3.1.7でテストしてみました。

$ yes jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj | head -10000000 >../k

$ time -p env LC_ALL=C gawk '/a|b/ { print }' ../k
real 2.65
user 2.58
sys 0.07

$ time -p env LC_ALL=ja_JP.eucJP gawk '/a|b/ { print }' ../k
real 13.96
user 13.86
sys 0.09

Gawk 4.1.1

執筆時点の最新版Gawk 4.1.1 (2014-04-08リリース)でテストしてみました。

$ time -p env LC_ALL=C ./gawk '/a|b/ { print }' ../k
real 2.29
user 2.22
sys 0.07

$ time -p env LC_ALL=ja_JP.eucJP ./gawk '/a|b/ { print }' ../k
real 5.37
user 5.28
sys 0.08

Gawk開発中版 (Git)

執筆時点で最新のスナップショットを取得してテストしてみました。

$ time -p env LC_ALL=C ./gawk '/a|b/ { print }' ../k
real 1.63
user 1.57
sys 0.05

$ time -p env LC_ALL=ja_JP.eucJP ./gawk '/a|b/ { print }' ../k
real 1.63
user 1.56
sys 0.07

まとめと解説

Cロケールで1.6倍、非UTF-8マルチバイト・ロケール(EUC-JP)では8.5倍もパフォーマンス向上していました。

ところで、パフォーマンス向上したといっても、この数値はGNU grepよりも4倍くらい遅いです。awkでは変数RSに値を設定することで行区切りを変更することができ*1、このハンドリングのために遅くなっています。

*1:Gawkでは正規表現を使用することも可能です。

bashの脆弱性 (ShellShock) まとめ

これまでに見つかり修正されているbash脆弱性 (ShellShock) について、該当有無の判断方法と、修正バージョンをまとめました。

CVE-2014-6271

確認方法
$ env x='() { :;}; echo vulnerable' ./bash -c "echo this is a test"

[NG pattern]
vulnerable
this is a test

[OK pattern]
./bash: warning: x: ignoring function definition attempt
./bash: error importing function definition for `x'
this is a test

or 

No output.
修正パッチ
  • bash43-025
  • bash42-048
  • bash41-012
  • bash40-039
  • bash32-052
  • bash31-018
  • bash30-017
  • bash205b-008

CVE-2014-7169

確認方法
$ env X='() { (a)=>\' ./bash -c "echo date"; cat echo

[NG pattern]
./bash: X: line 1: syntax error near unexpected token `='
./bash: X: line 1: `'
./bash: error importing function definition for `X'
Thu Oct  9 00:09:13 JST 2014

[OK pattern]
./bash: X: line 1: syntax error near unexpected token `='
./bash: X: line 1: `'
./bash: error importing function definition for `X'
date
cat: echo: No such file or directory

or 

No output.
修正パッチ
  • bash43-026
  • bash42-049
  • bash41-013
  • bash40-040
  • bash32-053
  • bash31-019
  • bash30-018
  • bash205b-009

CVE-2014-7186

確認方法
$ ./bash -c 'true <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF <<EOF'

[NG pattern]
Segmentation fault (core dumped)

[OK pattern]
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')

or 

No output.
修正パッチ
  • bash43-028
  • bash42-051
  • bash41-015
  • bash40-042
  • bash32-055
  • bash31-021
  • bash30-020
  • bash205b-011

CVE-2014-7187

確認方法
$ (for x in {1..200} ; do echo "for x$x in ; do :"; done; for x in {1..200} ; do echo done ; done) | ./bash

[NG pattern]
./bash: line 129: syntax error near `x129'
./bash: line 129: `for x129 in ; do :'

[OK pattern]
(for x in {1..200} ; do echo "for x$x in ; do :"; done; for x in {1..200} ; do echo done ; done) | ./bash

or 

No output.
修正パッチ
  • bash43-028
  • bash42-051
  • bash41-015
  • bash40-042
  • bash32-055
  • bash31-021
  • bash30-020
  • bash205b-011

CVE-2014-6277

確認方法
$ env 'BASH_FUNC_x%%=() { x() { _; }; x() { _; } <<a; }' ./bash -c ':'

[NG pattern]
Segmentation fault (core dumped)

[OK pattern]
./bash: warning: here-document at line 0 delimited by end-of-file (wanted `a')

or 

No output.
修正パッチ
  • bash43-029
  • bash42-052
  • bash41-016
  • bash40-043
  • bash32-056
  • bash31-022
  • bash30-021
  • bash205b-012

CVE-2014-6278

確認方法
$ env 'BASH_FUNC_x%%=() { _; } >_[$($())] { echo hi mom; id; }' ./bash -c ':'

[NG pattern]
hi mom
uid=500(staff) gid=100(users) groups=100(users)

[OK pattern]
./bash: x: line 0: syntax error near unexpected token `{'
./bash: x: line 0: `x () { _; } >_[$($())] { echo hi mom; id; }'
./bash: error importing function definition for `x'||<

or 

No output.
修正パッチ
  • bash43-030
  • bash42-053
  • bash41-017
  • bash40-044
  • bash32-057
  • bash31-023
  • bash30-022
  • bash205b-013

特記事項

下記のセキュリティ強化パッチを適用することで脆弱性が著しく緩和されます。

  • bash43-027
  • bash42-050
  • bash41-014
  • bash40-041
  • bash32-054
  • bash31-020
  • bash30-019
  • bash205b-010

bashの脆弱性CVE-2014-7186, CVE-2014-7187について

CVE-2014-6271やCVE-2014-7169と比べると影響は小さいですが、本家のGNUからは2014/09/30時点においてもパッチはリリースされていません。CVE-2014-6271の対処を済ませていれば発生条件を満たすスクリプトを書かない限り再現せず、その発生条件は通常の使用方法では満さないものであることから、GNUとしてもパッチのリリースを急いでいなかったのかもしれません。

CVE-2014-7186

下記のテストケースはCVE-2014-7186の不具合によりコア・ダンプしたり、フレーム・ポインターが書き換えられたりする可能性があります。

bash -c "true `yes '<<EOF' | head -20 | xargs echo`"

これはparse.yの中でリダイレクトの情報を積み上げるスタックとして使用している配列のサイズが10にハード・コーディングされていることが原因です。

static REDIRECT *redir_stack[10];

そのため、11以上ネストした場合にコア・ダンプしたり、フレーム・ポインターが書き換えられたりする可能性があります。

発生条件

ヒア・ドキュメントを11以上ネストして使用している場合に発生する可能性があります。

Red Hat社による修正

Red Hat社はこの不具合に対してパッチを提供しています。そのパッチでは、スタックをヒープ上に作成し不足した場合は追加の割り当てを行うように修正しています。

CVE-2014-7187

下記のテストケースはCVE-2014-7187の不具合によりエラー出力します。場合によってはコア・ダンプしたり、フレーム・ポインターが書き換えられたりする可能性があります。

bash -c "`yes 'for i in; do' | head -200; echo ':'; yes 'done' | head -200 `"

これも不具合箇所はparse.yです。

/* The line number in a script where the word in a `case WORD', `select WORD'
   or `for WORD' begins.  This is a nested command maximum, since the array
   index is decremented after a case, select, or for command is parsed. */
#define MAX_CASE_NEST   128
static int word_lineno[MAX_CASE_NEST];
static int word_top = -1;

    ........

    case FOR:
      if (word_top < MAX_CASE_NEST)
        word_top++;
      word_lineno[word_top] = line_number;
      break;

word_topの値が127の時に問題箇所を通過すると、word_top++で128になるため存在しない129番目の要素word_lineno[128]にアクセスしてします。その結果、コア・ダンプしたり、フレーム・ポインターが書き換えられたりする可能性があります。

発生条件

for、case、selectを128以上ネストしている場合に発生する可能性があります。

Red Hat社による修正

Red Hat社はこの不具合に対してパッチを提供しています。そのパッチでは、下記のように修正しています。

    case FOR:
      if (word_top + 1 < MAX_CASE_NEST)
        word_top++;
      word_lineno[word_top] = line_number;
      break;

2014/10/02追記

2014/10/02にCVE-2014-7186とCVE-2014-7187の両方をFixするパッチがリリースされました。 (bash43-028、他)

bash - 環境変数から関数を取得する機能のセキュリティ強化

CVE-2014-6271やCVE-2014-7169では、任意の環境変数定義で起こりうることが問題の1つでした。そこで、bashの起動時に設定された環境変数が関数なのか変数なのかを判断するロジックについて修正が行われました。

※このパッチはCVE-2014-6277とCVE-2014-6278を修正としてリリースされました。しかし、CVE-2014-6277, CVE-2014-6278は非公開のため詳細は不明です。

従来は環境変数の値の先頭4文字「() {」のみで関数か変数かを判断していましたが、修正後は環境変数の名前もチェックし、環境変数の名前が「BASH_FUNC_」で始まり、「%%」で終わる場合のみ関数とみなすように変更されました。

この修正によりセキュリティが強化されると同時に、同じ名前の関数と変数を環境変数から取得できるようになりました。

$ env foo=variable 'BASH_FUNC_foo%%=() { echo function; }' ./bash -c 'echo $foo; foo'
variable
function

それでは、実際に行われた修正内容を見ていくことにしましょう。(bash43-027)

+ #define BASHFUNC_PREFIX		"BASH_FUNC_"
+ #define BASHFUNC_PREFLEN	10	/* == strlen(BASHFUNC_PREFIX */
+ #define BASHFUNC_SUFFIX		"%%"
+ #define BASHFUNC_SUFFLEN	2	/* == strlen(BASHFUNC_SUFFIX) */

先ほど解説した、「BASH_FUNC_」と「%%」が定義されています。

***************
*** 350,369 ****
        /* If exported function, define it now.  Don't import functions from
  	 the environment in privileged mode. */
!       if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
  	{
  	  string_length = strlen (string);
! 	  temp_string = (char *)xmalloc (3 + string_length + char_index);
  
! 	  strcpy (temp_string, name);
! 	  temp_string[char_index] = ' ';
! 	  strcpy (temp_string + char_index + 1, string);
  
  	  /* Don't import function names that are invalid identifiers from the
  	     environment, though we still allow them to be defined as shell
  	     variables. */
! 	  if (legal_identifier (name))
! 	    parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
  
! 	  if (temp_var = find_function (name))
  	    {
  	      VSETATTR (temp_var, (att_exported|att_imported));
--- 355,385 ----
        /* If exported function, define it now.  Don't import functions from
  	 the environment in privileged mode. */
!       if (privmode == 0 && read_but_dont_execute == 0 && 
!           STREQN (BASHFUNC_PREFIX, name, BASHFUNC_PREFLEN) &&
!           STREQ (BASHFUNC_SUFFIX, name + char_index - BASHFUNC_SUFFLEN) &&
! 	  STREQN ("() {", string, 4))
  	{
+ 	  size_t namelen;
+ 	  char *tname;		/* desired imported function name */
+ 
+ 	  namelen = char_index - BASHFUNC_PREFLEN - BASHFUNC_SUFFLEN;
+ 
+ 	  tname = name + BASHFUNC_PREFLEN;	/* start of func name */
+ 	  tname[namelen] = '\0';		/* now tname == func name */
+ 
  	  string_length = strlen (string);
! 	  temp_string = (char *)xmalloc (namelen + string_length + 2);
  
! 	  memcpy (temp_string, tname, namelen);
! 	  temp_string[namelen] = ' ';
! 	  memcpy (temp_string + namelen + 1, string, string_length + 1);
  
  	  /* Don't import function names that are invalid identifiers from the
  	     environment, though we still allow them to be defined as shell
  	     variables. */
! 	  if (absolute_program (tname) == 0 && (posixly_correct == 0 || legal_identifier (tname)))
! 	    parse_and_execute (temp_string, tname, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
  
! 	  if (temp_var = find_function (tname))
  	    {
  	      VSETATTR (temp_var, (att_exported|att_imported));
***************
*** 378,383 ****
  		}
  	      last_command_exit_value = 1;
! 	      report_error (_("error importing function definition for `%s'"), name);
  	    }
  	}
  #if defined (ARRAY_VARS)
--- 394,402 ----
  		}
  	      last_command_exit_value = 1;
! 	      report_error (_("error importing function definition for `%s'"), tname);
  	    }
+ 
+ 	  /* Restore original suffix */
+ 	  tname[namelen] = BASHFUNC_SUFFIX[0];
  	}
  #if defined (ARRAY_VARS)

環境変数の名前が「BASH_FUNC_」で始まり、「%%」で終わる場合のみ関数とみなすように変更されています。例えば、環境変数BASH_FUNC_foo%%と定義すると、bash内ではその関数をfooとして参照できます。環境変数fooも定義しておけば、fooを変数としても参照でき、それらは明確に区別されます。また、absolute_program (tname) == 0 が追加されているので、名前に「/」を含む環境変数も関数とはみなされません。

*** 2955,2959 ****
  
    INVALIDATE_EXPORTSTR (var);
!   var->exportstr = mk_env_string (name, value);
  
    array_needs_making = 1;
--- 2974,2978 ----
  
    INVALIDATE_EXPORTSTR (var);
!   var->exportstr = mk_env_string (name, value, 0);
  
    array_needs_making = 1;
***************
*** 3853,3871 ****
  
  static inline char *
! mk_env_string (name, value)
       const char *name, *value;
  {
!   int name_len, value_len;
!   char	*p;
  
    name_len = strlen (name);
    value_len = STRLEN (value);
!   p = (char *)xmalloc (2 + name_len + value_len);
!   strcpy (p, name);
!   p[name_len] = '=';
    if (value && *value)
!     strcpy (p + name_len + 1, value);
    else
!     p[name_len + 1] = '\0';
    return (p);
  }
--- 3872,3911 ----
  
  static inline char *
! mk_env_string (name, value, isfunc)
       const char *name, *value;
+      int isfunc;
  {
!   size_t name_len, value_len;
!   char	*p, *q;
  
    name_len = strlen (name);
    value_len = STRLEN (value);
! 
!   /* If we are exporting a shell function, construct the encoded function
!      name. */
!   if (isfunc && value)
!     {
!       p = (char *)xmalloc (BASHFUNC_PREFLEN + name_len + BASHFUNC_SUFFLEN + value_len + 2);
!       q = p;
!       memcpy (q, BASHFUNC_PREFIX, BASHFUNC_PREFLEN);
!       q += BASHFUNC_PREFLEN;
!       memcpy (q, name, name_len);
!       q += name_len;
!       memcpy (q, BASHFUNC_SUFFIX, BASHFUNC_SUFFLEN);
!       q += BASHFUNC_SUFFLEN;
!     }
!   else
!     {
!       p = (char *)xmalloc (2 + name_len + value_len);
!       memcpy (p, name, name_len);
!       q = p + name_len;
!     }
! 
!   q[0] = '=';
    if (value && *value)
!     memcpy (q + 1, value, value_len + 1);
    else
!     q[1] = '\0';
! 
    return (p);
  }
***************
*** 3953,3957 ****
  	     using the cached exportstr... */
  	  list[list_index] = USE_EXPORTSTR ? savestring (value)
! 					   : mk_env_string (var->name, value);
  
  	  if (USE_EXPORTSTR == 0)
--- 3993,3997 ----
  	     using the cached exportstr... */
  	  list[list_index] = USE_EXPORTSTR ? savestring (value)
! 					   : mk_env_string (var->name, value, function_p (var));
  
  	  if (USE_EXPORTSTR == 0)

同じ名前で関数と変数をexportすることが可能になったため、bash内ではそれらを区別できる必要があります。そこで、mk_env_string関数にexportされたのが関数か変数かを区別するための第3引数isfuncが追加されました。もちろん、呼び出し元も修正されています。

bashに深刻な脆弱性 (CVE-2014-6271, CVE-2014-7169)

bashにコード・インジェクション攻撃を許す深刻な脆弱性が見つかりました。(CVE-2014-6271, CVE-2014-7169)

bashを起動する際に、環境変数に特殊な文字列を設定することで、任意の処理を起動することが可能になります。既に修正パッチの配布が開始されていて、GNUのダウンロード・サイトやOSのディストリビュータよりダウンロードできます。

CVE-2014-6271

それでは、CVE-2014-6271について、bashソースコード (variables.c) を追いながら見ていくことにしましょう。

/* Initialize the shell variables from the current environment.
   If PRIVMODE is nonzero, don't import functions from ENV or
   parse $SHELLOPTS. */
void
initialize_shell_variables (env, privmode)
     char **env;
     int privmode;
{
  char *name, *string, *temp_string;
  int c, char_index, string_index, string_length, ro;
  SHELL_VAR *temp_var;

  create_variable_tables ();

  for (string_index = 0; string = env[string_index++]; )
    {
      char_index = 0;
      name = string;
      while ((c = *string++) && c != '=')
        ;
      if (string[-1] == '=')
        char_index = string - name - 1;

      /* If there are weird things in the environment, like `=xxx' or a
         string without an `=', just skip them. */
      if (char_index == 0)
        continue;

      /* ASSERT(name[char_index] == '=') */
      name[char_index] = '\0';

      /* Now, name = env variable name, string = env variable value, and
         char_index == strlen (name) */

initialize_shell_variables関数では、まず起動時に設定された環境変数を順に読み取り、区切り文字である「=」で名前 (name) と値 (string) に分割しています。char_indexは区切り文字「=」がある位置を指しています。

      temp_var = (SHELL_VAR *)NULL;

      /* If exported function, define it now.  Don't import functions from
         the environment in privileged mode. */
      if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
        {
          string_length = strlen (string);
          temp_string = (char *)xmalloc (3 + string_length + char_index);

          strcpy (temp_string, name);
          temp_string[char_index] = ' ';
          strcpy (temp_string + char_index + 1, string);

環境変数の値 (string) の先頭から4文字が「() {」にマッチした場合は、名前 (name) 、半角スペース、値 (string) を順に連結したものを temp_string としています。例えば、環境変数 func の値が '() {echo abc}' であった場合、temp_string の内容は「func() {echo abc}」になります。

          if (posixly_correct == 0 || legal_identifier (name))
            parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);

そして、temp_string を bashスクリプトとして解析、実行しています。

この機能を使用することで、下記のようにシェル変数だけでなくシェル関数もサブ・シェルに送ることができます。

$ export func='() {echo abc}'
$ bash -c 'func'
abc

しかし、下記のコードを実行するとどうなるでしょうか。

$ env x='() {:;}; echo vulnerable'  bash -c 'echo this is a test'

temp_string の内容が「x() {:;}; echo vulnerable」となるので、解析時に「echo vulnerable」が実行されてしまいます。つまり、「echo vulnerable」を悪意のあるコマンドに置き換えることで、任意のコマンドを実行することができてしまいます。

修正パッチを適用することでチェックが厳しくなり、上記のような環境変数を定義しても警告が出力されるようになります。「echo vulnerable」は実行されません。

修正リリース
  • bash43-025
  • bash42-048
  • bash41-012
  • bash40-039
  • bash32-052
  • bash31-018
  • bash30-017
  • bash205b-008

CVE-2014-7169

CVE-2014-6271に対する修正パッチがリリースされたものの、下記のようなケースには対応していないとの指摘が上がりました。それがCVE-2014-7169です。

$ env X='() { (a)=>\' bash -c "echo date"; cat echo

# expected
./bash: X: line 0: syntax error near unexpected token `)'
./bash: X: line 0: `X () { ()=>\'
./bash: error importing function definition for `X'
date
cat: echo: No such file or directory

# actual
./bash: X: line 1: syntax error near unexpected token `='
./bash: X: line 1: `'
./bash: error importing function definition for `X'
Sat Sep 27 08:34:22 JST 2014

このケースでは、CVE-2014-7169の脆弱性により解析に失敗した時点で「>」に関する情報が残ってしまい、次の「echo date」を実行する際に、「>echo date」として解析、実行してしまいます。その結果、期待された動作は「date」と出力されechoという名前のファイルは作成されないことですが、実際にはdateコマンドの結果がechoという名前のファイルに出力されます。

もう少し詳細に説明すると、「\」に改行が続いた場合は継続行とみなされる必要があるため、前の文字が「>」であったことをeol_ungetc_lookahead変数に格納しておきます。しかし、コマンドはそこで終わっています。その結果、仕様通り解析エラーにはなりますが、不具合バージョンではeol_ungetc_lookahead変数の値をクリアすることなく「echo date」を解析、実行してしまいます。

いくつかのディストリビューションでは、このケースにも対応した修正パッチを配布しています。最新バージョンに対するGNUの修正 (bash43-026) は1箇所のみです。解析エラー時にコールされるparse.yのreset_parser関数で、eol_ungetc_lookahead変数の値をクリアするように変更されています。

修正リリース
  • bash43-026
  • bash42-049
  • bash41-013
  • bash40-040
  • bash32-053
  • bash31-019
  • bash30-018
  • bash205b-009

その他のケース

下記のようなケースは問題ないのでしょうか。

$ cat <<EOF >test.sh
#!/bin/bash
cat /dev/null
EOF

$ chmod a+x test.sh
$ env cat='() { echo rm -rf /; }' ./test.sh
rm -rf /

CVE-2014-6271やCVE-2014-7169の最大の問題点は下記の2点です。

  • 任意の環境変数定義で起こりうる。
  • 期待されていない部分が解析、実行される。

例えば、CGIではUser-Agentの値をHTTP_USER_AGENT環境変数にセットした状態でプログラムが起動されます。従って、User-Agentに'() {:;}; echo 'Content-Type=text/plain'; echo; /bin/cat /etc/passwd'をセットしてシェルで書かれたCGIスクリプトにWebアクセスすると何が起こりうるか、容易に想像していただけると思います。

上記のケースはこの2点のどちらも満たさないので、CVE-2014-6271やCVE-2014-7169と比較すると、さほど大きな問題ではありません。

その他、これまでセキュリティ脆弱性についてあまり考慮されてこなかったbashには、未修正のCVE-2014-7186, CVE-2014-7187をはじめ多数の脆弱性が潜んでいる可能性もあり、継続的に議論が進められています。

データベース設計 〜 マスタデータを含めて、全ての履歴を残したいという要望

データベースの「正規化」を学んで間もない頃に実際のデータベース設計をやってみて、悩んだことをお話をしたいと思います。

理想と現実の壁にぶち当たる

下記の属性を持つ「売上ファイル」を考えてみます。

売上ファイル
----------------
売上日
顧客名
住所
担当者名
商品名
単価
数量

これを第3正規形にすると、こんな感じになります。

売上テーブル
----------------
売上コード
売上日
顧客コード

売上明細テーブル
----------------
売上コード
商品コード
数量

顧客マスタ
----------
顧客コード
顧客名
住所
担当者名

商品マスタ
----------
商品コード
商品名
単価

ところが、このままでは商品マスタの単価が変更されると、売上金額が変わってしまいます。そこで、売上明細テーブルにも「単価」列を追加し、売上日時点の単価を設定するようにしました。*1

それだけならまだ良かったのですが、下記の2つの問題を突きつけられました。

  1. 顧客や商品が削除されてしまうと、売上一覧を作成した時にその部分が空欄になってしまう。
  2. 売上を集計するときは、顧客名、住所、担当者名、商品名も売上時点のものであって欲しい。(お客様の要望)

壁を乗り越えるために

結局、さらに逆正規化を進めて、全てのマスタテーブルの列を売上テーブルと売上明細テーブルに組み込みました。もはや、最初の「売上ファイル」とほとんど同じ状態です。

その後、1点目の問題を解決するために、マスタテーブルに「論理削除フラグ」を持たせることを考えつきました。*2顧客や商品の削除を論理削除フラグの変更で対処すれば、売上を集計した時にその部分が空白になってしまうことはありません。

しかし、2点目の問題は最後まで解決できず、「マスタの変更によって売上一覧の顧客名、住所、担当者名、商品名が変わってしまうのは制限事項である」として、お客様に渋々納得していただきました。

どうしていればよかったか

当時と今ではマシン性能もデータベース性能も異なるので、今の考え方を当時にそのまま当てはめることはできませんが、今の自分ならこんな感じにすると思います。

売上テーブル
----------------
売上コード
売上日
顧客履歴コード

売上明細テーブル
----------------
売上明細コード
売上コード
商品履歴コード
数量

顧客マスタ
----------
顧客コード
顧客履歴コード

顧客マスタ履歴
--------------
顧客履歴コード
顧客コード
顧客名
住所
担当者名

商品マスタ
----------------
商品コード
商品履歴コード

商品マスタ履歴
--------------
商品履歴コード
商品コード
商品名
単価

こうすることで、第3正規形や整合性をほとんど崩さず要件を満たすことができます。

マスタの更新は、マスタ履歴テーブルへの INSERT と、マスタテーブルの履歴コードの UPDATE です。少し手間が増えますが、マスタの更新処理は単純なので1つ作成すれば後は横展開できるでしょう。売上一覧についても、マスタテーブルを結合せず、マスタ履歴テーブルを結合するだけで作成できます。

参考までに、これを DDL に落とすと下記のようになります。

CREATE TABLE cust_hist (
  cust_hist_cd INTEGER,
  cust_cd INTEGER,
  cust_name VARCHAR(200),
  address VARCHAR(500),
  person_in_charge VARCHAR(200),
  CONSTRAINT pk_cust_hist PRIMARY KEY ( cust_hist_cd ),
  CONSTRAINT uk_cust_hist UNIQUE ( cust_cd, cust_hist_cd ));

CREATE TABLE cust (
  cust_cd INTEGER,
  cust_hist_cd INTEGER,
  CONSTRAINT pk_cust PRIMARY KEY ( cust_cd ),
  CONSTRAINT fk_cust FOREIGN KEY ( cust_cd, cust_hist_cd )
    REFERENCES cust_hist ( cust_cd, cust_hist_cd ));

CREATE TABLE item_hist (
  item_hist_cd DECIMAL,
  item_cd DECIMAL,
  unit_price DECIMAL,
  CONSTRAINT pk_item_hist PRIMARY KEY ( item_hist_cd ),
  CONSTRAINT uk_item_hist UNIQUE ( item_cd, item_hist_cd ));

CREATE TABLE item (
  item_cd DECIMAL,
  item_hist_cd DECIMAL,
  CONSTRAINT pk_item PRIMARY KEY ( item_cd ),
  CONSTRAINT fk_item FOREIGN KEY ( item_cd, item_hist_cd )
    REFERENCES item_hist ( item_cd, item_hist_cd ));

CREATE TABLE sales (
  sales_cd INTEGER,
  sales_at TIMESTAMP,
  cust_hist_cd INTEGER,
  CONSTRAINT pk_sales PRIMARY KEY ( sales_cd ),
  CONSTRAINT fk_cust_cust_hist FOREIGN KEY ( cust_hist_cd )
    REFERENCES cust_hist );

CREATE TABLE sales_detail (
  sales_detail_cd INTEGER,
  sales_cd INTEGER,
  item_hist_cd INTEGER,
  quantity INTEGER,
  CONSTRAINT pk_sales_detail PRIMARY KEY ( sales_detail_cd ),
  CONSTRAINT fk_sales_detail_sales FOREIGN KEY ( sales_cd )
    REFERENCES sales,
  CONSTRAINT fk_sales_detail_item_hist FOREIGN KEY ( item_hist_cd )
    REFERENCES item_hist );

複雑な要件をどう実装するかについては意見が分かれるところですが、1つの方法として参考にしていただければと思います。

*1:逆正規化の例としてよくある話です。

*2:独自で考えつきましたが、インターネットを検索してみると、既に同じことをやっている事例が多数見つかりました。

OpenSSL、今度はChange Cipher Specメッセージ処理に脆弱性

2014年4月に致命的ともいうべき HeartBleed 脆弱性 (CVE-2014-0160) が発見され大騒ぎを引き起こした OpenSSL。それからたった 2 ヶ月しか経過していないにもかかわらず、新たな脆弱性 (CVE-2014-0224) が発見されました。発見したのは株式会社レピダムの菊池正史氏とのこと、素晴らしい!

どのような脆弱性

SSL通信では、本通信の前に証明書を検証したり、共有鍵を生成して交換したりします。これをSSLハンドシェークと呼び、下図の順でメッセージを交換します。

クライアント                  サーバ
==================            ==================
ClientHello         ------->
                    <-------  ServerHello
                    <-------  Certificate*
                    <-------  ServerKeyExchange*
                    <-------  CertificateRequest*
                    <-------  ServerHelloDone
Certificate*        ------->
ClientKeyExchange   ------->
CertificateVerify*  ------->
[ChangeCipherSpec]  ------->
Finished            ------->
                    <-------  [ChangeCipherSpec]
                    <-------  Finished
Application Data    <------>  Application Data

今回の脆弱性が発見されたのは、ChangeCipherSpec の箇所です。ChangeCipherSpec は、以降の暗号化通信で使用する暗号化方式を決定するためのメッセージです。ChangeCipherSpec は、必ず上記の位置で送受信される必要があるにもかかわらず、上記の位置以外でも受信できるようになっていたことが問題とのことです。ClientKeyExchange のセキュリティ・パラメータを受け取っていない状態で攻撃者から ChangeCipherSpec を送られると、暗号化鍵は空のパラメータから生成され、誰もが容易に推測できる状態になってしまうとのことです。

どのように修正されたか

Gitリポジトリ上でソースコードを確認してみました。ソースコードの取得は下記のコマンドで行えます。*1

$ git clone git://github.com/openssl/openssl.git
$ cd openssl
$ git diff a7c682fb6f692c9a3868777a7ff305784714c131 a91be10833e61bcdc9002de28489405101c52650
diff --git a/ssl/s3_clnt.c b/ssl/s3_clnt.c
index 5fc9069..34efff8 100644
--- a/ssl/s3_clnt.c
+++ b/ssl/s3_clnt.c
@@ -599,6 +599,7 @@ int ssl3_connect(SSL *s)
 		case SSL3_ST_CR_FINISHED_A:
 		case SSL3_ST_CR_FINISHED_B:
 
+			s->s3->flags |= SSL3_FLAGS_CCS_OK;
 			ret=ssl3_get_finished(s,SSL3_ST_CR_FINISHED_A,
 				SSL3_ST_CR_FINISHED_B);
 			if (ret <= 0) goto end;
@@ -1051,6 +1052,7 @@ int ssl3_get_server_hello(SSL *s)
 		SSLerr(SSL_F_SSL3_GET_SERVER_HELLO,SSL_R_ATTEMPT_TO_REUSE_SESSION_IN_DIFFERENT_CONTEXT);
 		goto f_err;
 		}
+	    s->s3->flags |= SSL3_FLAGS_CCS_OK;
 	    s->hit=1;
 	    }
 	else	/* a miss or crap from the other end */
diff --git a/ssl/s3_pkt.c b/ssl/s3_pkt.c
index 34eb2b4..fb9720f 100644
--- a/ssl/s3_pkt.c
+++ b/ssl/s3_pkt.c
@@ -1593,6 +1593,15 @@ start:
 			goto f_err;
 			}
 
+		if (!(s->s3->flags & SSL3_FLAGS_CCS_OK))
+			{
+			al=SSL_AD_UNEXPECTED_MESSAGE;
+			SSLerr(SSL_F_SSL3_READ_BYTES,SSL_R_CCS_RECEIVED_EARLY);
+			goto f_err;
+			}
+
+		s->s3->flags &= ~SSL3_FLAGS_CCS_OK;
+
 		rr->length=0;
 
 		if (s->msg_callback)
diff --git a/ssl/s3_srvr.c b/ssl/s3_srvr.c
index 72fd3e4..31bfe47 100644
--- a/ssl/s3_srvr.c
+++ b/ssl/s3_srvr.c
@@ -708,6 +708,7 @@ int ssl3_accept(SSL *s)
 		case SSL3_ST_SR_CERT_VRFY_A:
 		case SSL3_ST_SR_CERT_VRFY_B:
 
+			s->s3->flags |= SSL3_FLAGS_CCS_OK;
 			/* we should decide if we expected this one */
 			ret=ssl3_get_cert_verify(s);
 			if (ret <= 0) goto end;
@@ -735,6 +736,7 @@ int ssl3_accept(SSL *s)
 
 		case SSL3_ST_SR_FINISHED_A:
 		case SSL3_ST_SR_FINISHED_B:
+			s->s3->flags |= SSL3_FLAGS_CCS_OK;
 			ret=ssl3_get_finished(s,SSL3_ST_SR_FINISHED_A,
 				SSL3_ST_SR_FINISHED_B);
 			if (ret <= 0) goto end;
@@ -805,7 +807,10 @@ int ssl3_accept(SSL *s)
 				s->s3->tmp.next_state=SSL3_ST_SR_FINISHED_A;
 #else
 				if (s->s3->next_proto_neg_seen)
+					{
+					s->s3->flags |= SSL3_FLAGS_CCS_OK;
 					s->s3->tmp.next_state=SSL3_ST_SR_NEXT_PROTO_A;
+					}
 				else
 					s->s3->tmp.next_state=SSL3_ST_SR_FINISHED_A;
 #endif
diff --git a/ssl/ssl3.h b/ssl/ssl3.h
index 8bd201e..82dd76c 100644
--- a/ssl/ssl3.h
+++ b/ssl/ssl3.h
@@ -428,6 +428,7 @@ typedef struct ssl3_buffer_st
 #define TLS1_FLAGS_TLS_PADDING_BUG		0x0008
 #define TLS1_FLAGS_SKIP_CERT_VERIFY		0x0010
 #define TLS1_FLAGS_KEEP_HANDSHAKE		0x0020
+#define SSL3_FLAGS_CCS_OK			0x0080
  
 /* SSL3_FLAGS_SGC_RESTART_DONE is set when we
  * restart a handshake because of MS SGC and so prevents us

ChangeCipherSpecを受信してもよいタイミングであるかどうかを判定するためのフラグSSL3_FLAGS_CCS_OKを追加し、フラグがONになっていないときにChangeCipherSpecを受信するとエラーを返すように変更されたようです。

*1:gitプロトコルの代わりにhttpsプロトコルを使用することも可能です。