Java ゴッチャ - ビット演算子とブール演算子

2021年02月07日掲載
アラン・リチャードソン著
ケーススタディ

Java ゴッチャ - ビット演算子とブール演算子

2021年02月07日掲載
アラン・リチャードソン著
リソースを見る
リソースを見る

Java ゴッチャ - ビット演算子とブール演算子

> "Java Gotcha" - 誤って実装してしまいがちなミスパターン。

Javaの簡単な失敗例として、ブール比較演算子の代わりにBitwise演算子を使用することがあります。

例えば、単純なミスタイプで、本当は「&&」と書きたかったのに「&」と書いてしまうことがあります。

コードをレビューするときに学ぶ一般的なヒューリスティックな考え方があります。

条件文の中で「&」や「|」が使われている場合は、おそらく意図したものではないでしょう。

このブログ記事では、ヒューリスティックを探り、このコーディング問題を特定して修正する方法を紹介します。


何が問題なのか?ビット演算はブーリアンでも問題なく使える


ビット演算子をブール値で使用することは完全に有効であり、Javaは構文エラーを報告しません。

JUnit Test でビットワイズ OR (|) とビットワイズ AND (&) の真理値表を調べると、ビットワイズ演算子の出力が真理値表と一致することがわかります。このように考えると、Bitwise 演算子の使用は問題ないと思われるかもしれません。

AND真理値表

aで3列、bで1列、最後に(a^b)で1列。


@Test
    void bitwiseOperatorsAndTruthTable(){
          Assertions.assertEquals(true, true & true);
          Assertions.assertEquals(false, true & false);
          Assertions.assertEquals(false, false & true);
          Assertions.assertEquals(false, false & false);
    }


テストはパス、これは完全に妥当なJavaです。


OR真理値表


aで3列、bで1列、最後に(a v b)で1列。


   @Test
    void bitwiseOperatorsOrTruthTable(){
        Assertions.assertEquals(true, true | true);
        Assertions.assertEquals(true, true | false);
        Assertions.assertEquals(true, false | true);
        Assertions.assertEquals(false, false | false);
    }


このテストもパスしていますが、なぜ「&&'」と「||'」を好むのでしょうか?


真理値表イメージは 真理値表ツールweb.standfor.edu.


問題点短絡動作


実際に問題となるのは、ビット演算子(&, |)とブール演算子(&&, ||)の動作の違いです。

ブール演算子は短絡的な演算子で、必要な分だけ評価します。

e.g.

if (args != null & args.length() > 23) {
    System.out.println(args);
}


上記のコードでは、ビットワイズ演算子が使用されているため、両方のブール条件が評価されます。

  • args != null
  • args.length() > 23

これでは、argsがnullの場合にNullPointerExceptionが発生する可能性があります。なぜなら、argsがnullの場合でも、args.lengthのチェックを常に行うからです。


ブール演算子 短絡評価


&&が使われている場合は、例えば

if (args != null && args.length() > 23) {
    System.out.println(args);
}


args != nullがfalseと評価されたことがわかると、すぐに条件式の評価を停止します。

右辺の評価は必要ありません。

右辺の条件の結果がどうであれ、ブール式の最終的な値は「偽」になります。


しかし、これはプロダクションコードではあり得ないことです。


これは非常に犯しやすいミスで、静的解析ツールでは必ずしも検出されません。

このパターンの公開例がないか、以下のGoogle Dorkを使って調べてみました。

filetype:java if "!=null & "
今回の検索では、AndroidのRootWindowContainerのコードが出てきました
isDocument = intent != null & intent.isDocument()


代入文の中でビットワイズ演算子を使って値をマスクすることはよくあるので、このようなコードはコードレビューを通過する可能性があります。しかし、この例では、上記のif文の例と同じ結果になります。intentがnullの場合は、NullPointerExceptionがスローされます。

このような構造になってしまうのは、防御的なコードを書いてしまい、冗長なコードを書いてしまうことが多いからです。!=nullのチェックは、ほとんどのユースケースでは余計なことかもしれません。

これはプログラマーがプロダクションコードで犯したエラーです。

検索結果がどれくらい最新かはわかりませんが、私が検索したときには、Google、Amazon、Apache...そして私のコードが戻ってきました。

私のオープンソースプロジェクトの一つで、最近行われたプルリクエストは、まさにこのエラーに対処するものでした。

if(type!=null & type.trim().length()>0){
    acceptMediaTypeDefinitionsList.add(type.trim());
}


これを見つけるには


私のサンプルコードをいくつかの静的アナライザーでチェックしたところ、どのアナライザーもこの隠れた自爆コードを拾いませんでした。

Secure Code Warrior のチームとして、これを拾うことができる、かなりシンプルなSensei のレシピを作成し、検討しました。

ビット演算子は完全に有効であり、代入にもよく使用されるため、問題のあるコードを見つけるために、if文の使用例とビット演算子の使用に焦点を当てました。

search:
  expression:
    anyOf:
    - in:
        condition: {}
    value:
      caseSensitive: false
      matches: ".* & .*"


これは、" & " が条件式として使われている場合に、正規表現を使ってマッチさせるものです。

この問題を解決するために、再び正規表現を利用しました。今回はQuickFixのsed関数を使って、式の中の&を&&にグローバルに置き換えました。

availableFixes:
  - name: "Replace bitwise AND operator to logical AND operator"
    actions:
      - rewrite:
          to: "{{#sed}}s/&/&&/g,{{{ . }}}{{/sed}}"


エンドノート

ここでは、ビットワイズ演算子の最も一般的な誤用、つまり、実際にはブール演算子を意図していた場合について説明します。

課題の例のように、このような状況は他にもありますが、レシピを書く際には、誤認識を避けるようにしなければなりません。私たちは、最も一般的な現象に合わせてレシピを作成します。Sensei が進化するにつれ、より多くの一致する条件をカバーするために、検索機能にさらなる特異性を追加する可能性があります。

現在の形では、このレシピは多くのライブユースケースを特定し、最も重要なのは、私のプロジェクトで報告されたものです。

注:この例題とレシピのレビューには、Charlie Eriksen、Matthieu Calie、Robin Claerhaut、Brysen Ackx、Nathan Desmet、Downey Robersscheutenなど、かなりの数のコードウォリアーが協力してくれました。ご協力ありがとうございました。


---


IntelliJの「Preferences ‾ Plugins」(Mac)または「Settings ‾ Plugins」(Windows)から、「sensei secure code」を検索して、「Sensei 」をインストールできます。

Secure Code Warrior GitHub アカウントの `sensei-blog-examples` リポジトリには、これらのブログ記事(今回の記事を含む)のソースコードやレシピが多数用意されています。

https://github.com/securecodewarrior/sensei-blog-examples

についてはこちらをご覧ください。Sensei


リソースを見る
リソースを見る

著者

アラン・リチャードソン

もっと知りたい?

セキュアコーディングに関する最新の知見をブログでご紹介しています。

当社の豊富なリソースライブラリは、安全なコーディングアップスキルに対する人間的なアプローチを強化することを目的としています。

ブログを見る
もっと知りたい?

開発者主導のセキュリティに関する最新の研究成果を入手する

ホワイトペーパーからウェビナーまで、開発者主導のセキュアコーディングを始めるために役立つリソースが満載のリソースライブラリです。今すぐご覧ください。

リソース・ハブ

Java ゴッチャ - ビット演算子とブール演算子

2021年02月07日掲載
アラン・リチャードソン著

Java ゴッチャ - ビット演算子とブール演算子

> "Java Gotcha" - 誤って実装してしまいがちなミスパターン。

Javaの簡単な失敗例として、ブール比較演算子の代わりにBitwise演算子を使用することがあります。

例えば、単純なミスタイプで、本当は「&&」と書きたかったのに「&」と書いてしまうことがあります。

コードをレビューするときに学ぶ一般的なヒューリスティックな考え方があります。

条件文の中で「&」や「|」が使われている場合は、おそらく意図したものではないでしょう。

このブログ記事では、ヒューリスティックを探り、このコーディング問題を特定して修正する方法を紹介します。


何が問題なのか?ビット演算はブーリアンでも問題なく使える


ビット演算子をブール値で使用することは完全に有効であり、Javaは構文エラーを報告しません。

JUnit Test でビットワイズ OR (|) とビットワイズ AND (&) の真理値表を調べると、ビットワイズ演算子の出力が真理値表と一致することがわかります。このように考えると、Bitwise 演算子の使用は問題ないと思われるかもしれません。

AND真理値表

aで3列、bで1列、最後に(a^b)で1列。


@Test
    void bitwiseOperatorsAndTruthTable(){
          Assertions.assertEquals(true, true & true);
          Assertions.assertEquals(false, true & false);
          Assertions.assertEquals(false, false & true);
          Assertions.assertEquals(false, false & false);
    }


テストはパス、これは完全に妥当なJavaです。


OR真理値表


aで3列、bで1列、最後に(a v b)で1列。


   @Test
    void bitwiseOperatorsOrTruthTable(){
        Assertions.assertEquals(true, true | true);
        Assertions.assertEquals(true, true | false);
        Assertions.assertEquals(true, false | true);
        Assertions.assertEquals(false, false | false);
    }


このテストもパスしていますが、なぜ「&&'」と「||'」を好むのでしょうか?


真理値表イメージは 真理値表ツールweb.standfor.edu.


問題点短絡動作


実際に問題となるのは、ビット演算子(&, |)とブール演算子(&&, ||)の動作の違いです。

ブール演算子は短絡的な演算子で、必要な分だけ評価します。

e.g.

if (args != null & args.length() > 23) {
    System.out.println(args);
}


上記のコードでは、ビットワイズ演算子が使用されているため、両方のブール条件が評価されます。

  • args != null
  • args.length() > 23

これでは、argsがnullの場合にNullPointerExceptionが発生する可能性があります。なぜなら、argsがnullの場合でも、args.lengthのチェックを常に行うからです。


ブール演算子 短絡評価


&&が使われている場合は、例えば

if (args != null && args.length() > 23) {
    System.out.println(args);
}


args != nullがfalseと評価されたことがわかると、すぐに条件式の評価を停止します。

右辺の評価は必要ありません。

右辺の条件の結果がどうであれ、ブール式の最終的な値は「偽」になります。


しかし、これはプロダクションコードではあり得ないことです。


これは非常に犯しやすいミスで、静的解析ツールでは必ずしも検出されません。

このパターンの公開例がないか、以下のGoogle Dorkを使って調べてみました。

filetype:java if "!=null & "
今回の検索では、AndroidのRootWindowContainerのコードが出てきました
isDocument = intent != null & intent.isDocument()


代入文の中でビットワイズ演算子を使って値をマスクすることはよくあるので、このようなコードはコードレビューを通過する可能性があります。しかし、この例では、上記のif文の例と同じ結果になります。intentがnullの場合は、NullPointerExceptionがスローされます。

このような構造になってしまうのは、防御的なコードを書いてしまい、冗長なコードを書いてしまうことが多いからです。!=nullのチェックは、ほとんどのユースケースでは余計なことかもしれません。

これはプログラマーがプロダクションコードで犯したエラーです。

検索結果がどれくらい最新かはわかりませんが、私が検索したときには、Google、Amazon、Apache...そして私のコードが戻ってきました。

私のオープンソースプロジェクトの一つで、最近行われたプルリクエストは、まさにこのエラーに対処するものでした。

if(type!=null & type.trim().length()>0){
    acceptMediaTypeDefinitionsList.add(type.trim());
}


これを見つけるには


私のサンプルコードをいくつかの静的アナライザーでチェックしたところ、どのアナライザーもこの隠れた自爆コードを拾いませんでした。

Secure Code Warrior のチームとして、これを拾うことができる、かなりシンプルなSensei のレシピを作成し、検討しました。

ビット演算子は完全に有効であり、代入にもよく使用されるため、問題のあるコードを見つけるために、if文の使用例とビット演算子の使用に焦点を当てました。

search:
  expression:
    anyOf:
    - in:
        condition: {}
    value:
      caseSensitive: false
      matches: ".* & .*"


これは、" & " が条件式として使われている場合に、正規表現を使ってマッチさせるものです。

この問題を解決するために、再び正規表現を利用しました。今回はQuickFixのsed関数を使って、式の中の&を&&にグローバルに置き換えました。

availableFixes:
  - name: "Replace bitwise AND operator to logical AND operator"
    actions:
      - rewrite:
          to: "{{#sed}}s/&/&&/g,{{{ . }}}{{/sed}}"


エンドノート

ここでは、ビットワイズ演算子の最も一般的な誤用、つまり、実際にはブール演算子を意図していた場合について説明します。

課題の例のように、このような状況は他にもありますが、レシピを書く際には、誤認識を避けるようにしなければなりません。私たちは、最も一般的な現象に合わせてレシピを作成します。Sensei が進化するにつれ、より多くの一致する条件をカバーするために、検索機能にさらなる特異性を追加する可能性があります。

現在の形では、このレシピは多くのライブユースケースを特定し、最も重要なのは、私のプロジェクトで報告されたものです。

注:この例題とレシピのレビューには、Charlie Eriksen、Matthieu Calie、Robin Claerhaut、Brysen Ackx、Nathan Desmet、Downey Robersscheutenなど、かなりの数のコードウォリアーが協力してくれました。ご協力ありがとうございました。


---


IntelliJの「Preferences ‾ Plugins」(Mac)または「Settings ‾ Plugins」(Windows)から、「sensei secure code」を検索して、「Sensei 」をインストールできます。

Secure Code Warrior GitHub アカウントの `sensei-blog-examples` リポジトリには、これらのブログ記事(今回の記事を含む)のソースコードやレシピが多数用意されています。

https://github.com/securecodewarrior/sensei-blog-examples

についてはこちらをご覧ください。Sensei


弊社製品や関連するセキュアコーディングのトピックに関する情報をお送りする許可をお願いします。当社は、お客様の個人情報を細心の注意を払って取り扱い、マーケティング目的で他社に販売することは決してありません。

送信
フォームを送信するには、「Analytics」のCookieを有効にしてください。完了したら、再度無効にしてください。