Angularのセキュリティ対策について調べてみる その1
はじめに
フロントエンドで行うセキュリティ対策について、Angularではどのようにしているのかなぁ〜、と疑問に思ったので調べてみました。
まずはAngularの公式ドキュメントを元に、あれこれ試してみることにします。
https://angular.io/guide/security
雑な翻訳で進めているため、間違いなどありましたらご指摘のほどよろしくお願いいたしますm( )m
XSSについて
ドキュメントではまずXSSへの対応について書かれています。
Angularではプロパティやスタイルなどの値はTemplateから挿入され、それらの値は全てデフォルトでサニタイズされます。
そのため、仮にユーザーが入力した値をそのままページ上に表示したとしても、Javascriptなどのコードは実行されません。
Angularのセキュリティの文脈で扱われる要素は以下の4つです。
- HTML
- Style (CSS)
- URL (例:< a href>)
- Resource URL (例:< script src>)
untrustedなコードがサニタイズされた場合、ConsoleにはWarningが出力されます。
ということで、src/app/inner-html-binding.component.htmlを試してみました。
< h3>Binding innerHTML < p>Bound value:< /p> < p>{{htmlSnippet}}< /p> < p>Result of binding to innerHTML:< /p> < p [innerHTML]="htmlSnippet">< /p>
- クラスは今回特に関係がないと考えたため、簡略化のため省いています。
- {{htmlSnippet}}と[innerHTML]に後述のHTMLなどを含めた値を渡し、どのように反映されるかを確認します。
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-sample-from-docs', templateUrl: './sample-from-docs.component.html', styleUrls: ['./sample-from-docs.component.css'] }) export class SampleFromDocsComponent implements OnInit { htmlSnippet = 'Template < script>alert("0wned")< /script>< b>Syntax< /b>'; constructor() { } ngOnInit() { } }
- htmlSnippetにHTMLタグを含めてどう表示されるかを確認しています。
上記はブログ内でHTMLタグが反映されないように「<」の後にスペースを入れていますが、実際には削除してください。
結果
{{htmlSnippet}}
- 入力されたHTMLタグは反映されず、記号も含めてそのまま表示される。
[innerHTML]
- scriptタグは除去され、bタグは反映される。
色々試してみた
[innerHTML]の方はセキュリティ上問題ないとされたHTMLタグは反映されました。
では、他にどのようなものが反映されるか、または反映されないのかを調べてみました。
〜省略〜 export class SampleFromDocsComponent implements OnInit { htmlSnippet = '< div class="testclass" id="testid" style="color:red;">Test< br>Test2\nTest3< /div>< font color=red>Test4< /font>< ul>< li>t< /li>< li>e< /li>< /ul>< h1>H1< /h1>< img src="#" onerror=alert(0) alt="test" />< video src="#">< /video>< a href="/">Top< /a>< a href="javascript:alert(0)">script< /a>'; constructor() { } ngOnInit() { } }
結果
{{htmlSnippet}}
- 先程と同じくタグは全て反映されず、記号も含めて入力された値がそのまま表示される。
[innerHTML]
反映されたもの
* divタグ
* class
* brタグ
* fontタグ
* ulタグ
* liタグ
* h1タグ
* imgタグ
* videoタグ
* aタグ
反映されなかったもの
* id
* style
* 改行文字(\n)
* onerror
* href="javascript:alert(0)"
* iframeタグ
- 反映されなかったものの大部分はただ除去されただけですが、「href=“javascript:alert(0)"」は「href="unsafe:javascript:alert(0)"」に置き換えられ、
リンクをクリックすると「xdg-openを開きますか?」と聞かれ、「xdg-openを開く」としてもJavascriptの実行はされませんでした。 - classが反映されてidが除去されるのが謎ですね。。。何かidに関連する脆弱性があったのでしょうか。
- classは反映されるものの、例えばCSSにこのクラスに対して何かを書いても反映はされませんでした。おそらくCSSと上記コードの実行順によるものだと思います。
- aタグはそのまま反映されるということで、ユーザーの入力した値をそのまま反映する場合は、クッションのページなどを挟んだ方が良いかも知れません。
FormsModuleについて
ドキュメントからは外れますが、Formに入力されたデータも見てみました。
app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { SampleFromDocsComponent } from './sample-from-docs/sample-from-docs.component'; import { SampleForFormComponent } from './sample-for-form/sample-for-form.component'; @NgModule({ declarations: [ AppComponent, SampleFromDocsComponent, SampleForFormComponent ], imports: [ BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
- ngModelを使って2 way bindingを行うために、FormsModuleのインポートが必要です。
sample-for-form.component.ts
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-sample-for-form', templateUrl: './sample-for-form.component.html', styleUrls: ['./sample-for-form.component.css'] }) export class SampleForFormComponent implements OnInit { private inputValue: string; constructor() { } ngOnInit() { } onSubmitButtonClicked(){ console.log(this.inputValue); } }
- 入力された値を保持する変数と、ボタンをクリックした時に入力されたデータをログに出力するようにしました。
sample-for-form.component.html
< textarea [(ngModel)]='inputValue'>< /textarea> < textarea [(ngModel)]='inputValue'>< /textarea> < div>{{inputValue}}< /div> < button (click)='onSubmitButtonClicked()'>Submit< /button>
- 2つある内どちらかのTextareaに値を入力すると、もう一方にも反映される、という内容です。
確認内容
HTMLタグ、改行、絵文字を入力して、2 way bindingでの動作とボタン押下時のログを見てみます。
結果
HTMLタグ、絵文字はそのまま、改行も反映された状態で出力されました。
ということで、HTTPヘッダ、メールヘッダにユーザーが入力した値をそのまま反映する場合、
改行を取り除く必要があります(HTTPヘッダインジェクション、メールヘッダインジェクション防止のため)。
ま、そもそも値をそのまま反映するな、という話ではありますが。
暗号化
Angularに限ったことではありませんが、例えばログインを行う場合、
入力されたパスワードは暗号化してサーバー側に投げるのかな?と思っていました。
が、実際にはSSLで第三者に読み取られないようにした上で、サーバー側で必要に応じて暗号化を行う、というのが主流のようです。
ま、そりゃそうか。
ちなみにJavasciriptで暗号化とググるとCrypto.jsと、
Web Crypto APIが出てきます。
こちらもおいおい調べていきたいところ。
おわりに
結局ドキュメントからは話が逸れてしまいましたが、XSSについてはデフォルトでも対策がとられていることがわかりました。
自分でサニタイズをするかどうかは、AngularのTemplateだけを使って値を表示する場合は不要ですが、
そうでない場合はサニタイズしておくか、またはユーザーが入力した値がそのまま保存されていることを明示しておいた方が良さそうです。
次はドキュメントの続きを。。。