SPAも巡回可能！AeyeScanの自動巡回技術について

AeyeScan自動巡回エンジン開発担当の安西です。
近年、SPA(シングルページアプリケーション)の技術を使用して構築されたサイトを見かける頻度が急速に増えてきました。AeyeScanはブラウザベースのクローラーを搭載しているため、SPAも高精度で巡回することが可能です。
本記事では「OWASP Juice Shop」を例にAeyeScanの自動巡回技術の一部を解説します。

# OWASP Juice Shopとは
[OWASP Juice Shop](https://github.com/juice-shop/juice-shop)はセキュリティ教育やセキュリティツールの性能検証などを活用目的とするオープンソースの脆弱なWebアプリです。SPAフレームワークのAngularを使用しており、以下の技術的特徴があります。

- フォームにformタグが無い
- フレームワークの独自タグを使用している
- マウス操作やキー押下が必要
- CAPTCHA突破が必要
- モーダルが多い

## フォームにformタグが無い
従来型のWebアプリは入力フォームの内容をformタグで括るのが一般的です。

【従来型のシンプルなログインフォーム】
```
<form action="login.php" method="post">
ユーザID:<input type="text" name="user_id"/>
パスワード:<input type="password" name="password"/>
<input type="submit" value="ログイン"/>
</form>
```
一方、SPAでは入力フォームをformタグで括らないケースが多くあります。OWASP Juice Shopでもformタグは使用されていません。

【OWASP Juice Shopのログインフォーム】
![スクリーンショット 2022-06-15 14.21.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/88eaf7d5-5783-8c1b-da10-a8b324643926.png)

【OWASP Juice ShopのログインフォームのHTML】
```
<div _ngcontent-qkp-c158="" id="login-form" class="form-container"><mat-form-field _ngcontent-qkp-c158="" color="accent" appearance="outline" class="mat-form-field ng-tns-c118-7 mat-accent mat-form-field-type-mat-input mat-form-field-appearance-outline mat-form-field-can-float mat-form-field-has-label mat-form-field-hide-placeholder ng-untouched ng-pristine ng-invalid ng-star-inserted"><div class="mat-form-field-wrapper ng-tns-c118-7"><div class="mat-form-field-flex ng-tns-c118-7"><div class="mat-form-field-outline ng-tns-c118-7 ng-star-inserted"><div class="mat-form-field-outline-start ng-tns-c118-7" style="width: 5.5px;"></div><div class="mat-form-field-outline-gap ng-tns-c118-7" style="width: 42.25px;"></div><div class="mat-form-field-outline-end ng-tns-c118-7"></div></div><div class="mat-form-field-outline mat-form-field-outline-thick ng-tns-c118-7 ng-star-inserted"><div class="mat-form-field-outline-start ng-tns-c118-7" style="width: 5.5px;"></div><div class="mat-form-field-outline-gap ng-tns-c118-7" style="width: 42.25px;"></div><div class="mat-form-field-outline-end ng-tns-c118-7"></div></div><!----><!----><!----><div class="mat-form-field-infix ng-tns-c118-7"><input _ngcontent-qkp-c158="" id="email" name="email" matinput="" aria-label="Text field for the login email" class="mat-input-element mat-form-field-autofill-control ng-tns-c118-7 ng-untouched ng-pristine ng-invalid cdk-text-field-autofill-monitored" required="" data-placeholder="" aria-required="true"><span class="mat-form-field-label-wrapper ng-tns-c118-7"><label class="mat-form-field-label ng-tns-c118-7 mat-empty mat-form-field-empty mat-accent ng-star-inserted" id="mat-form-field-label-3" for="email" aria-owns="email"><!----><mat-label _ngcontent-qkp-c158="" translate="" class="ng-tns-c118-7 ng-star-inserted">Email</mat-label><!----><span aria-hidden="true" class="mat-placeholder-required mat-form-field-required-marker ng-tns-c118-7 ng-star-inserted"> *</span><!----></label><!----></span></div><!----></div><!----><div class="mat-form-field-subscript-wrapper ng-tns-c118-7"><!----><div class="mat-form-field-hint-wrapper ng-tns-c118-7 ng-trigger ng-trigger-transitionMessages ng-star-inserted" style="opacity: 1; transform: translateY(0%);"><!----><div class="mat-form-field-hint-spacer ng-tns-c118-7"></div></div><!----></div></div></mat-form-field><mat-form-field _ngcontent-qkp-c158="" color="accent" appearance="outline" class="mat-form-field ng-tns-c118-8 mat-accent mat-form-field-type-mat-input mat-form-field-appearance-outline mat-form-field-can-float mat-form-field-has-label mat-form-field-hide-placeholder ng-untouched ng-pristine ng-invalid ng-star-inserted"><div class="mat-form-field-wrapper ng-tns-c118-8"><div class="mat-form-field-flex ng-tns-c118-8"><div class="mat-form-field-outline ng-tns-c118-8 ng-star-inserted"><div class="mat-form-field-outline-start ng-tns-c118-8" style="width: 5.5px;"></div><div class="mat-form-field-outline-gap ng-tns-c118-8" style="width: 63.25px;"></div><div class="mat-form-field-outline-end ng-tns-c118-8"></div></div><div class="mat-form-field-outline mat-form-field-outline-thick ng-tns-c118-8 ng-star-inserted"><div class="mat-form-field-outline-start ng-tns-c118-8" style="width: 5.5px;"></div><div class="mat-form-field-outline-gap ng-tns-c118-8" style="width: 63.25px;"></div><div class="mat-form-field-outline-end ng-tns-c118-8"></div></div><!----><!----><!----><div class="mat-form-field-infix ng-tns-c118-8"><input _ngcontent-qkp-c158="" id="password" name="password" matinput="" aria-label="Text field for the login password" class="mat-input-element mat-form-field-autofill-control ng-tns-c118-8 ng-untouched ng-pristine ng-invalid cdk-text-field-autofill-monitored" type="password" required="" data-placeholder="" aria-required="true"><span class="mat-form-field-label-wrapper ng-tns-c118-8"><label class="mat-form-field-label ng-tns-c118-8 mat-empty mat-form-field-empty mat-accent ng-star-inserted" id="mat-form-field-label-5" for="password" aria-owns="password"><!----><mat-label _ngcontent-qkp-c158="" translate="" class="ng-tns-c118-8 ng-star-inserted">Password</mat-label><!----><span aria-hidden="true" class="mat-placeholder-required mat-form-field-required-marker ng-tns-c118-8 ng-star-inserted"> *</span><!----></label><!----></span></div><div class="mat-form-field-suffix ng-tns-c118-8 ng-star-inserted"><button _ngcontent-qkp-c158="" mat-icon-button="" matsuffix="" aria-label="Button to display the password" mattooltipposition="right" class="mat-focus-indicator mat-tooltip-trigger mat-icon-button mat-button-base ng-tns-c118-8 ng-star-inserted" aria-describedby="cdk-describedby-message-1" cdk-describedby-host="0"><span class="mat-button-wrapper"><svg _ngcontent-qkp-c158="" aria-label="Eye" class="svg-inline--fa fa-eye fa-w-18" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" data-fa-i2svg=""><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg><!-- <i _ngcontent-qkp-c158="" aria-label="Eye" class="fas fa-eye"></i> Font Awesome fontawesome.com --></span><span matripple="" class="mat-ripple mat-button-ripple mat-button-ripple-round"></span><span class="mat-button-focus-overlay"></span></button><!----><!----><!----></div><!----></div><!----><div class="mat-form-field-subscript-wrapper ng-tns-c118-8"><!----><div class="mat-form-field-hint-wrapper ng-tns-c118-8 ng-trigger ng-trigger-transitionMessages ng-star-inserted" style="opacity: 1; transform: translateY(0%);"><!----><div class="mat-form-field-hint-spacer ng-tns-c118-8"></div></div><!----></div></div></mat-form-field><a _ngcontent-qkp-c158="" routerlink="/forgot-password" translate="" class="primary-link forgot-pw" href="#/forgot-password">Forgot your password?</a><button _ngcontent-qkp-c158="" type="submit" id="loginButton" mat-raised-button="" color="primary" aria-label="Login" class="mat-focus-indicator mat-raised-button mat-button-base mat-primary mat-button-disabled" disabled="true"><span class="mat-button-wrapper"><mat-icon _ngcontent-qkp-c158="" role="img" class="mat-icon notranslate material-icons mat-icon-no-color" aria-hidden="true" data-mat-icon-type="font"> exit_to_app </mat-icon> Log in </span><span matripple="" class="mat-ripple mat-button-ripple"></span><span class="mat-button-focus-overlay"></span></button><mat-checkbox _ngcontent-qkp-c158="" id="rememberMe" class="mat-checkbox mat-accent ng-untouched ng-pristine ng-valid"><label class="mat-checkbox-layout" for="rememberMe-input"><span class="mat-checkbox-inner-container"><input type="checkbox" class="mat-checkbox-input cdk-visually-hidden" id="rememberMe-input" tabindex="0" aria-label="Checkbox to stay logged in or not logged in" aria-checked="false"><span matripple="" class="mat-ripple mat-checkbox-ripple mat-focus-indicator"><span class="mat-ripple-element mat-checkbox-persistent-ripple"></span></span><span class="mat-checkbox-frame"></span><span class="mat-checkbox-background"><svg version="1.1" focusable="false" viewBox="0 0 24 24" xml:space="preserve" aria-hidden="true" class="mat-checkbox-checkmark"><path fill="none" stroke="white" d="M4.1,12.7 9,17.6 20.3,6.3" class="mat-checkbox-checkmark-path"></path></svg><span class="mat-checkbox-mixedmark"></span></span></span><span class="mat-checkbox-label"><span style="display: none;">&nbsp;</span> Remember me </span></label></mat-checkbox><!----><!----><div _ngcontent-qkp-c158="" id="newCustomerLink"><a _ngcontent-qkp-c158="" routerlink="/register" translate="" class="primary-link" href="#/register">Not yet a customer?</a></div></div>
```

多くの自動巡回ツールは、formタグを解析しaction属性のURLにinput要素などのパラメータ情報を付加してPOSTするという方法でサイトを巡回します。
```
<form action="login.php" method="post">
ユーザID:<input type="text" name="user_id"/>
パスワード:<input type="password" name="password"/>
<input type="submit" value="ログイン"/>
</form>
↓
POST /login.php HTTP/1.1
略
user_id=testuser&password=testpass
```
しかし、formタグの無いサイトではこの方法は上手く動作しません。
```
<div _ngcontent-qkp-c158="" id="login-form" class="form-container"><mat-form-field _ngcontent-qkp-c158="" color="accent" appearance="outline" class="mat-form-field ng-tns-c118-7 mat-accent mat-form-field-type-mat-input mat-form-field-appearance-outline mat-form-field-can-float mat-form-field-has-label mat-form-field-hide-placeholder ng-untouched ng-pristine ng-invalid ng-star-inserted"><div class="mat-form-field-wrapper ng-tns-c118-7"><div class="mat-form-field-flex ng-tns-c118-7"><div class="mat-form-field-outline ng-tns-c118-7 ng-star-inserted"><div class="mat-form-field-outline-start ng-tns-c118-7" style="width: 5.5px;"></div><div class="mat-form-field-outline-gap ng-tns-c118-7" style="width: 42.25px;"></div><div class="mat-form-field-outline-end ng-tns-c118-7"></div></div><div class="mat-form-field-outline mat-form-field-outline-thick ng-tns-c118-7 ng-star-inserted"><div class="mat-form-field-outline-start ng-tns-c118-7" style="width: 5.5px;"></div><div class="mat-form-field-outline-gap ng-tns-c118-7" style="width: 42.25px;"></div><div class="mat-form-field-outline-end ng-tns-c118-7"></div></div><!----><!----><!----><div class="mat-form-field-infix ng-tns-c118-7"><input _ngcontent-qkp-c158="" id="email" name="email" matinput="" aria-label="Text field for the login email" class="mat-input-element mat-form-field-autofill-control ng-tns-c118-7 ng-untouched ng-pristine ng-invalid cdk-text-field-autofill-monitored" required="" data-placeholder="" aria-required="true"><span class="mat-form-field-label-wrapper ng-tns-c118-7"><label class="mat-form-field-label ng-tns-c118-7 mat-empty mat-form-field-empty mat-accent ng-star-inserted" id="mat-form-field-label-3" for="email" aria-owns="email"><!----><mat-label _ngcontent-qkp-c158="" translate="" class="ng-tns-c118-7 ng-star-inserted">Email</mat-label><!----><span aria-hidden="true" class="mat-placeholder-required mat-form-field-required-marker ng-tns-c118-7 ng-star-inserted"> *</span><!----></label><!----></span></div><!----></div><!----><div class="mat-form-field-subscript-wrapper ng-tns-c118-7"><!----><div class="mat-form-field-hint-wrapper ng-tns-c118-7 ng-trigger ng-trigger-transitionMessages ng-star-inserted" style="opacity: 1; transform: translateY(0%);"><!----><div class="mat-form-field-hint-spacer ng-tns-c118-7"></div></div><!----></div></div></mat-form-field><mat-form-field _ngcontent-qkp-c158="" color="accent" appearance="outline" class="mat-form-field ng-tns-c118-8 mat-accent mat-form-field-type-mat-input mat-form-field-appearance-outline mat-form-field-can-float mat-form-field-has-label mat-form-field-hide-placeholder ng-untouched ng-pristine ng-invalid ng-star-inserted"><div class="mat-form-field-wrapper ng-tns-c118-8"><div class="mat-form-field-flex ng-tns-c118-8"><div class="mat-form-field-outline ng-tns-c118-8 ng-star-inserted"><div class="mat-form-field-outline-start ng-tns-c118-8" style="width: 5.5px;"></div><div class="mat-form-field-outline-gap ng-tns-c118-8" style="width: 63.25px;"></div><div class="mat-form-field-outline-end ng-tns-c118-8"></div></div><div class="mat-form-field-outline mat-form-field-outline-thick ng-tns-c118-8 ng-star-inserted"><div class="mat-form-field-outline-start ng-tns-c118-8" style="width: 5.5px;"></div><div class="mat-form-field-outline-gap ng-tns-c118-8" style="width: 63.25px;"></div><div class="mat-form-field-outline-end ng-tns-c118-8"></div></div><!----><!----><!----><div class="mat-form-field-infix ng-tns-c118-8"><input _ngcontent-qkp-c158="" id="password" name="password" matinput="" aria-label="Text field for the login password" class="mat-input-element mat-form-field-autofill-control ng-tns-c118-8 ng-untouched ng-pristine ng-invalid cdk-text-field-autofill-monitored" type="password" required="" data-placeholder="" aria-required="true"><span class="mat-form-field-label-wrapper ng-tns-c118-8"><label class="mat-form-field-label ng-tns-c118-8 mat-empty mat-form-field-empty mat-accent ng-star-inserted" id="mat-form-field-label-5" for="password" aria-owns="password"><!----><mat-label _ngcontent-qkp-c158="" translate="" class="ng-tns-c118-8 ng-star-inserted">Password</mat-label><!----><span aria-hidden="true" class="mat-placeholder-required mat-form-field-required-marker ng-tns-c118-8 ng-star-inserted"> *</span><!----></label><!----></span></div><div class="mat-form-field-suffix ng-tns-c118-8 ng-star-inserted"><button _ngcontent-qkp-c158="" mat-icon-button="" matsuffix="" aria-label="Button to display the password" mattooltipposition="right" class="mat-focus-indicator mat-tooltip-trigger mat-icon-button mat-button-base ng-tns-c118-8 ng-star-inserted" aria-describedby="cdk-describedby-message-1" cdk-describedby-host="0"><span class="mat-button-wrapper"><svg _ngcontent-qkp-c158="" aria-label="Eye" class="svg-inline--fa fa-eye fa-w-18" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" data-fa-i2svg=""><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg><!-- <i _ngcontent-qkp-c158="" aria-label="Eye" class="fas fa-eye"></i> Font Awesome fontawesome.com --></span><span matripple="" class="mat-ripple mat-button-ripple mat-button-ripple-round"></span><span class="mat-button-focus-overlay"></span></button><!----><!----><!----></div><!----></div><!----><div class="mat-form-field-subscript-wrapper ng-tns-c118-8"><!----><div class="mat-form-field-hint-wrapper ng-tns-c118-8 ng-trigger ng-trigger-transitionMessages ng-star-inserted" style="opacity: 1; transform: translateY(0%);"><!----><div class="mat-form-field-hint-spacer ng-tns-c118-8"></div></div><!----></div></div></mat-form-field><a _ngcontent-qkp-c158="" routerlink="/forgot-password" translate="" class="primary-link forgot-pw" href="#/forgot-password">Forgot your password?</a><button _ngcontent-qkp-c158="" type="submit" id="loginButton" mat-raised-button="" color="primary" aria-label="Login" class="mat-focus-indicator mat-raised-button mat-button-base mat-primary mat-button-disabled" disabled="true"><span class="mat-button-wrapper"><mat-icon _ngcontent-qkp-c158="" role="img" class="mat-icon notranslate material-icons mat-icon-no-color" aria-hidden="true" data-mat-icon-type="font"> exit_to_app </mat-icon> Log in </span><span matripple="" class="mat-ripple mat-button-ripple"></span><span class="mat-button-focus-overlay"></span></button><mat-checkbox _ngcontent-qkp-c158="" id="rememberMe" class="mat-checkbox mat-accent ng-untouched ng-pristine ng-valid"><label class="mat-checkbox-layout" for="rememberMe-input"><span class="mat-checkbox-inner-container"><input type="checkbox" class="mat-checkbox-input cdk-visually-hidden" id="rememberMe-input" tabindex="0" aria-label="Checkbox to stay logged in or not logged in" aria-checked="false"><span matripple="" class="mat-ripple mat-checkbox-ripple mat-focus-indicator"><span class="mat-ripple-element mat-checkbox-persistent-ripple"></span></span><span class="mat-checkbox-frame"></span><span class="mat-checkbox-background"><svg version="1.1" focusable="false" viewBox="0 0 24 24" xml:space="preserve" aria-hidden="true" class="mat-checkbox-checkmark"><path fill="none" stroke="white" d="M4.1,12.7 9,17.6 20.3,6.3" class="mat-checkbox-checkmark-path"></path></svg><span class="mat-checkbox-mixedmark"></span></span></span><span class="mat-checkbox-label"><span style="display: none;">&nbsp;</span> Remember me </span></label></mat-checkbox><!----><!----><div _ngcontent-qkp-c158="" id="newCustomerLink"><a _ngcontent-qkp-c158="" routerlink="/register" translate="" class="primary-link" href="#/register">Not yet a customer?</a></div></div>
↓
リクエストを生成できない
```

AeyeScanでは、ブラウザを実際に起動し、表示された入力フォームに対して値を入力、ボタンを押下することによってこの課題を解決しています。
以下のようなSeleniumのテストスクリプトを自動で生成するイメージが近いです。
```
# ログインIDを入力
login_id = driver.find_element_by_id("email")
login_id.send_keys("指定されたログインID")
# パスワードを入力
password = driver.find_element_by_id("password")
password.send_keys("指定されたパスワード")
# ログインボタンをクリック
login_btn = driver.find_element_by_id("loginButton")
login_btn.click()
```
スクリプトを自動生成し、自動実行する。この繰り返しにより、ブラウザベースの自動巡回を実現しています。

## フレームワークの独自タグを使用している
ブラウザベースの自動巡回では、入力可能なフォーム要素や、押下できるボタンを探すことが課題となります。
探索手法はいくつかありますが、全てのタグを対象に無条件に試行する手法では速度面の問題が発生するため、対象タグを絞り込む必要があります。
例えば、
- selectタグ、inputタグ、textareaタグなどを入力可能なフォーム要素とする
- buttonタグ、imgタグ、input type="button"などを押下可能なボタンとする

などが、候補になります。

AngularにはMaterial Designに対応した「Angular Material」というコンポーネントがあり、OWASP Juice Shopでも利用されています。
Angular MaterialにはHTML5には定義されていない独自タグが多数あります。一例として、selectタグの代替えタグである[MAT-SELECT](https://material.angular.io/components/select/overview)を使用している、OWASP Juice Shopのユーザ登録フォームのHTMLを記載します。
```
<mat-select _ngcontent-avu-c131="" role="combobox" aria-autocomplete="none" aria-haspopup="true" placeholder="" name="securityQuestion" aria-label="Selection list for the security question" class="mat-select ng-tns-c130-35 ng-tns-c118-34 mat-select-required mat-select-empty ng-pristine ng-invalid ng-star-inserted mat-select-invalid ng-touched" id="mat-select-6" tabindex="0" aria-expanded="false" aria-required="true" aria-disabled="false" aria-invalid="true" aria-describedby="mat-error-7"><div cdk-overlay-origin="" class="mat-select-trigger ng-tns-c130-35"><div class="mat-select-value ng-tns-c130-35" id="mat-select-value-7"><span class="mat-select-placeholder mat-select-min-line ng-tns-c130-35 ng-star-inserted"></span><!----><!----></div><div class="mat-select-arrow-wrapper ng-tns-c130-35"><div class="mat-select-arrow ng-tns-c130-35"></div></div></div><!----></mat-select>
```

SPAの巡回精度を向上させるには、上に挙げたinputなどの一般的なタグだけでなく、このようなフレームワーク独自タグの対応も必要となります。しかしながら、SPAフレームワークおよびコンポーネントは、React, Vue.js, Angular、Angular Material, Material-UIなど、メジャーなものだけでもかなりの数があるため、簡単な作業ではありません。

AeyeScanはSPAの巡回対応に注力しており、サポート対象とする独自タグを随時拡張していく予定です。

## マウス操作やキー押下が必要
OWASP Juice Shopにはマウス操作やキー押下をしないと巡回できない機能が複数あります。

- 検索実行にはEnterキーの押下が必要
![スクリーンショット 2022-06-14 12.32.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/98b6316d-a21a-a81e-4e66-d5f20f7bd70c.png)

- チャット投稿にはEnterキーの押下が必要
![スクリーンショット 2022-06-14 12.24.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/caa3578f-06c6-3ea3-bb6d-94cd51bc3ce3.png)

- Rating指定にはマウス操作が必要
![スクリーンショット 2022-06-14 12.56.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/f16a3c82-efc5-cb05-deb1-128913c34db4.png)

これらの機能を巡回するためには、マウス操作やキー押下をブラウザ上で実行する必要があります。
AeyeScanの巡回エンジンは、Enterキーの押下、スライダーのマウス操作、スクロールバー付き要素の自動スクロールなどにも対応しており、このような画面も巡回することが可能です。

【チャット投稿機能（AeyeScan巡回結果より）】
![スクリーンショット 2022-06-21 14.04.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/21360dfa-bb0a-6338-223e-48bce2f0924a.png)

ただし、実行条件の自動判定はとても難しいため、精度には向上の余地があります。

## CAPTCHA突破が必要
OWASP Juice ShopにはCAPTCHA突破が必要な投稿機能があります。
AeyeScanにはAIを用いたCAPTCHA突破の基盤があるため、正しい答えを入力することが出来ました。

【CAPTHCA付き入力フォーム（AeyeScan巡回結果より）】
![スクリーンショット 2022-06-21 14.11.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/e36e1a37-62b4-341b-346b-516c050d52d1.png)


この例は、四則演算の答えを入力する簡易なCAPTCHAですが、より難易度の高い画像CAPTHCAも突破することが可能です。画像CAPTHCAの突破技術については、[深層学習でCAPTCHA突破](https://qiita.com/AeyeScan/items/bf6d39fc1d63d3c12cf7)の記事で解説しています。

## モーダルが多い
ブラウザベースのクローラーは、ブラウザに描画された入力フォームやボタンを操作するという特性から、モーダルの扱いが大きな課題になります。操作対象の要素とモーダルが被る場合、要素の操作が上手くいかないためです。

【フォームの上にモーダルが表示される場合、フォームに値を入力できない】
![スクリーンショット 2022-06-14 13.06.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/edc71297-78eb-e58e-133f-5b79b62a1f5f.png)

AeyeScanでは、「モーダルを自動判定しモーダルの中身を優先的に巡回する」「モーダルの中身の巡回が終わったら、モーダルを閉じてから巡回を継続する」という方法でこの課題を解決しています。

【モーダルを閉じてから巡回する例（AeyeScan巡回結果より）】
![スクリーンショット 2022-06-21 14.19.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/2c9a7079-c8b2-1955-ea64-e473b3d489f1.png)

ただし、モーダルの自動判定はとても難しいため、精度には向上の余地があります。

# 自動巡回結果
OWASP Juice ShopをAeyeScanで自動巡回した結果を以下にまとめます。
AeyeScanは2022/6/24リリースの最新バージョンを使用しました。

## 巡回設定
|項目|内容|備考|
|---|---|---|
|OWASP Juice Shopバージョン|v13.3.0||
|AeyeScan設定 > 言語|英語||
|AeyeScan設定 > 速度|通常||
|AeyeScan設定 > アプリケーション認証情報|`admin@juice-sh.op` / `admin123`|OWASP Juice Shopインストール時に自動登録される管理ユーザ|

## 巡回結果
ほぼ全ての画面を高速に見つけ出すことが出来ました！

- 巡回時間: 11分
- 巡回画面数: 150画面
- 巡回率: 99%(削除系の機能など仕様により巡回しないページを除く)」

【機能一覧と巡回結果（重複除く)】
|#|機能名|PATH|巡回結果|特徴|
|---|---|---|---|---|
|1|ログイン前 TOP|/#/|o||
|2|ログイン前 Welcomeモーダル閉じる|/#/|o|モーダル|
|3|ログイン前 クッキー同意モーダル閉じる|/#/|o|モーダル|
|4|ログイン前 ログインフォーム|/#/login|o||
|5|ログイン前 検索結果|/#/search?q=a|o|Enterキーの押下が必要|
|6|ログイン前 会社概要|/#/about|o||
|7|ログイン前 利用規約|/ftp/legal.md|o||
|8|ログイン前 写真|/#/photo-wall|o||
|9|ログイン前 Help getting started|/#/|o||
|10|ログイン前 GitHub|/redirect?to=https&#58;//github.com/bkimminich/juice-shop|o||
|11|ログイン前 フィードバックフォーム|/#/contact|o||
|12|ログイン前 フィードバック完了|/#/contact|o|CAPTCHA有り|
|13|ログイン前 言語切り替えフォーム|/#/|-|仕様による除外|
|14|ログイン前 言語切り替え完了|/#/|-|仕様による除外|
|15|ログイン前 商品詳細|/#/|o||
|16|ログイン前 商品詳細 レビュー展開|/#/|o||
|17|ログイン前 商品リスト件数変更|/#/|o|独自タグを使用|
|18|ログイン前 商品リストページ送り|/#/|o||
|19|ログイン前 パスワード再設定フォーム|/#/forgot-password|o||
|20|ログイン前 パスワード再設定完了|/#/forgot-password|-|仕様による除外|
|21|ログイン前 ユーザー登録フォーム|/#/register|o||
|22|ログイン前 ユーザー登録完了|/#/login|o|formタグ無し|
|23|ログイン|/#/search|o|formタグ無し|
|24|買い物かごに追加|/#/search|o||
|25|商品詳細|/#/search|o||
|26|商品詳細 レビュー展開|/#/search|o||
|27|商品詳細 レビュー投稿完了|/#/search|o||
|28|商品詳細 レビュー編集|/#/search|o||
|29|商品詳細 レビュー編集完了|/#/search|o||
|30|商品詳細 いいね|/#/search|o||
|31|フィードバックフォーム|/#/contact|o||
|32|フィードバック完了|/#/contact|o|CAPTCHA有り|
|33|苦情フォーム|/#/complain|o||
|34|苦情投稿完了|/#/complain|o||
|35|チャット|/#/chatbot|o||
|36|チャット投稿完了|/#/chatbot|o|Enterキー押下が必要|
|37|会社概要|/#/about|o||
|38|利用規約|/ftp/legal.md|o||
|39|写真/#/photo-wall|o||
|40|写真投稿完了|/#/photo-wall|o|ラップされた入力要素有り|
|41|デラックスメンバー|/#/deluxe-membership|o||
|42|Help getting started|/#/|o||
|43|GitHub|/redirect?to=https&#58;//github.com/bkimminich/juice-shop|o||
|44|買い物かご|/#/basket|o|セッションストレージを使用|
|45|買い物かご商品数量変更|/#/basket|o||
|46|買い物かご商品削除|/#/basket|-|仕様による除外|
|47|注文 住所選択フォーム|/#/address/select|o||
|48|注文 住所追加フォーム|/#/address/create|o||
|49|注文 住所追加完了|/#/address/select|o||
|50|注文 配送速度選択フォーム|/#/delivery-method|o||
|51|注文 支払い方法選択フォーム|/#/payment/shop|o||
|52|注文 支払い方法選択フォーム クーポンフォーム展開|/#/payment/shop|o||
|53|注文 支払い方法選択フォーム クーポン適用完了|/#/payment/shop|x|正しいクーポンコードの入力が必要|
|54|注文 支払い方法選択フォーム オプション展開|/#/payment/shop|o||
|55|注文 支払い方法選択フォーム オプション展開 Spreadshirt(US)|/redirect?to=http&#58;//shop.spreadshirt.com/juiceshop|o|GitHubと類似|
|56|注文 支払い方法選択フォーム オプション展開 Spreadshirt(DE)|/redirect?to=http&#58;//shop.spreadshirt.de/juiceshop|o|GitHubと類似|
|57|注文 支払い方法選択フォーム オプション展開 StickerYou|/redirect?to=https&#58;//www.stickeryou.com/products/owasp-juice-shop/794|o|GitHubと類似|
|58|注文 支払い方法選択フォーム オプション展開 Learnpub|/redirect?to=http&#58;//leanpub.com/juice-shop|o|GitHubと類似|
|59|注文確認|/#/order-summary|o||
|60|注文完了|/#/order-completion/5550-35b09e15bca0da9d|o||
|61|注文書PDF|/ftp/order_5550-47d903fbe9e5be93.pdf|o||
|62|プロフィール|/profile|o||
|63|ユーザ名変更完了|/profile|o||
|64|アバター画像アップロード完了|/profile|o||
|65|アバターURL投稿完了|/profile|o||
|66|注文履歴|/#/order-history|o||
|67|配送状況|/#/track-result?id=5550-47d903fbe9e5be93|o||
|68|リサイクルボックスフォーム|/#/recycle|o||
|69|リサイクルボックス投稿完了|/#/recycle|o|独自タグを使用|
|70|住所一覧|/#/address/saved|o||
|71|住所追加フォーム|/#/address/create|o||
|72|住所追加完了|/#/address/saved|o||
|73|住所編集フォーム|/#/address/edit/7|o||
|74|住所編集完了|/#/address/saved|o||
|75|住所削除完了|/#/address/saved|-|仕様による除外|
|76|支払い方法一覧|/#/saved-payment-methods|o||
|77|クレジットカードフォーム|/#/saved-payment-methods|o||
|78|クレジットカード登録完了|/#/saved-payment-methods|o||
|79|支払い方法一覧 クレジットカード削除完了|/#/saved-payment-methods|-|仕様による除外|
|80|デジタル財布 金額入力フォーム|/#/wallet|o||
|81|デジタル財布 支払い方法選択フォーム|/#/payment/wallet|o||
|82|デジタル財布入金完了|/#/wallet|o||
|83|プライバシーポリシー|/#/privacy-security/privacy-policy|o||
|84|データエクスポートフォーム|/#/privacy-security/data-export|o||
|85|データエクスポート完了|/#/privacy-security/data-export|o||
|86|データ消去フォーム|/dataerasure|-|仕様による除外|
|87|データ消去完了|/dataerasure|-|仕様による除外|
|88|パスワード変更フォーム|/#/privacy-security/change-password|o||
|89|パスワード変更完了|/#/privacy-security/change-password|o||
|90|2要素認証登録フォーム|/#/privacy-security/two-factor-authentication|o||
|91|2要素認証登録完了|/#/privacy-security/two-factor-authentication|-|仕様による除外|
|92|2要素認証解除フォーム|/#/privacy-security/two-factor-authentication|-|仕様による除外|
|93|2要素認証解除完了|/#/privacy-security/two-factor-authentication|-|仕様による除外|
|94|前回ログイン時のIPアドレス|/#/privacy-security/last-login-ip|o||
|95|ログアウト|/#/|o||

## おわりに
以上、OWASP Juice Shopを例にAeyeScanの自動巡回技術の一部を解説しました。
本記事では触れなかったスキャン結果については、別の機会に改めて解説したいと思います。

AeyeScanのトライアル、脆弱性診断の自動化のご相談は[こちら](https://www.aeyescan.jp)
弊社ではエンジニア採用を実施中です。自動巡回技術に興味を持った方からの[ご応募](https://www.wantedly.com/projects/885912)もお待ちしています。