セキュアバイデザイン を読んでいるので、備忘メモ
1/2の続き
Chapter 8: Delivery Pipeline conscious of security
- デリバリパイプラインを自動化する
- 単体テスト
- 正常値テスト
- 境界値テスト
- 異常値テスト
- 極端値テスト
機能トグルのテスト
public class OrderService {
public void placeOrder(final Order order) {
notNull(order)
if (OrderMode.OLD.equals(toggleService.orderMode())) {
orderBackend.process(order); // 旧機能
}
else if (OrderMode.NEW.equals(toggleService.orderMode())) {
orderBackend.process(order);
biBackend.record(order); // 新機能
}
else {
throw new Exception("");
}
}
}
public class ToggleService {
public enum OrderMode {
OLD("old"),
NEW("new");
private final String key;
OrderMode(final String key) {
this.key = key;
}
public String key() {
return key;
}
}
private OrderMode orderMode = OLD;
public OrderMode orderMode() {
return orderMode;
}
public void setOrderMode(final OrderMode orderMode) {
this.orderMode = notNull(orderMode);
}
}
public class OrderServiceToggleTests {
@Test
public void should_process_order_if_old_order_mode_is_enabled() {
givenOrderModeIs(OLD);
whenPlacingAnOrder();
thenOrderShouldBeProcessed();
}
@Test
public void should_not_send_to_BI_if_old_order_mode_is_enabled() {
givenOrderModeIs(OLD);
whenPlacingAnOrder();
thenOrderShouldNotBeSentToBI(); // OLDならBIに送らない
}
@Test
public void should_process_order_if_new_order_mode_is_enabled() {
givenOrderModeIs(NEW);
whenPlacingAnOrder();
thenOrderShouldBeProcessed();
}
@Test
public void should_send_to_BI_if_new_order_mode_is_enabled() {
givenOrderModeIs(NEW);
whenPlacingAnOrder();
thenOrderShouldBeSentToBI(); // NEWならBIに送る
}
private ToggleService toggleService;
private OrderBackend orderBackend;
private BIBackend biBackend;
private void givenOrderModeIs(final OrderMode orderMode) {
toggleService = new ToggleService();
toggleService.setOrderMode(orderMode);
}
private whenPlacingAnOrder() {
createOrderService().placeOrder(new Order());
}
private OrderService createOrderService() {
orderBackend = mock(OrderBackend.class);
biBackend = mock(BIBackend.class);
return new OrderService(orderBackend, biBackend, toggleService);
}
private void thenOrderShouldBeProcessed() {
verify(orderBackend).process(any(Order.class));
}
private void thenOrderShouldNotBeSentToBI() {
verifyZeroInteractions(biBackend);
}
private void thenOrderShouldBeSentToBI() {
verify(biBackend).record(any(Order.class));
}
}
- 機能トグルの監視
- 許可されたユーザーだけアクセスさせる
- 誰が、いつ、トグルを変更したかログに残す
- セキュリティテスト
- アプリケーション
- インフラ
- 可用性のテスト
- (負荷テストとかNetflixのカオステストとかかな)
- ドメインルールを悪用した利用に対する検証
- 設定の妥当性確認
- 設定のホットスポット: システムの安全性に対して直接的、間接的に影響を及ぼす
- 暗黙的な振る舞いの検証
// 有効となっているHTTPメソッドのテスト
public class OnlyExpectedMethodsAreEnabledTest {
enum HTTPMethod {
GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE
}
URI uri;
List<Result> results;
@Test
public void verify_only_expected_HTTP_methods_are_enabled() {
givenEndpoint("http://example.com/endpoint");
whenTestingMethods(HTTPMethod.values());
thenTheOnlyMethodsEnabledAre(GET, PUT, HEAD);
}
void givenEndpoint(final String uri) {
this.uri = URI.create(uri);
}
void whenTestingMethods(final HTTPMethod... methods) {
results = Arrays.stream(methods)
.distinct()
.
}
}
Chapter 9: strategy of process failure considering safety
失敗の取り扱い
- ビジネス例外と技術的例外の分離
- デフォルトのエラーページを用意して、メッセージを表示する
private final AccountRepository repository;
public abstract class AccountException extends RuntimeException {}
public class AccountNotFound extends AccountException {
private final AccountNumber accountNumber;
private final Customer customer;
public AccountNotFound(final AccountNumber accountNumber,
final Customer customer) {
this.accountNumber = notNull(accountNumber);
this.customer = notNull(customer);
}
//略
}
public Account fetchAccountFor(final Customer customer,
final AccountNumber accountNumber) {
notNull(customer);
notNull(accountNumber);
try {
return accountDatabase
.selectAccountsFor(customer)
.stream()
.filter(account -> account.number().equals(accountNumber))
.findFirst()
.orElseThrow( // ビジネス例外
() -> new AccountNotFound(accountNumber, customer));
} catch (SQLException e) {
throw new IllegalStateException(
String.format("Unable to retrieve account %s for %s",
accountNumber.value(), customer), e);
}
}
public Balance accountBalance(final Customer customer,
final AccountNumber accountNumber) {
notNull(customer);
notNull(accountNumber);
try {
return repository.fetchAccountFor(customer, accountNumber)
.ballance();
} catch (AccountNotFound e) { // 想定されるビジネス例外
return Balance.unknown(accountNumber);
} catch (AccountException e) { // 想定外のビジネス例外
throw new IllegalStateException(
String.format("Unhandled domain exception: %s",
e.getClass().getSimpleName()));
}
}
-
findFirstメソッドではなく、Stream APIのreduceメソッドを検討する- e.g.
recude((accountA, accountB) → throw new IllegalStateException
- e.g.
例外に含める情報
- ReadOnceオブジェクトを活用して、機密情報が例外出力されないようにする
- 失敗は例外的なものか否か
- 例外的でないとして設計すると、ハンドリングや情報セキュリティの負担を軽減できる
// 例外を使わずに失敗を想定できる結果として設計する場合
// 返り値から判断する
// RustのResult型っぽい
public final class Account {
public Result transfer(final Amount amount, final Account toAccount {
notNull(amount);
notNull(toAccount);
if (balance().isLessThan(amount)) {
return INSUFFICIENT_FUNDS.failure();
}
return excecuteTransfer(amount, toAccount);
}
public Amount balance() {
return calculateBalance();
}
}
public final class Result {
public enum Failure { // 想定される失敗の種類
INSUFFICIENT_FUNDS,
SERVICE_NOT_AVAILABLE,
}
public static Result success() {
return new Result(this);
}
private final Failure failure;
private Result(final Failure failure) {
this.failure = failure; }
public boolean isFailure() {
return failure != null; }
public boolean isSuccess() {
return !isFailure(); }
public Optional<Failure> failure() {
return Optional.ofNullable(failure); }
}
To Be Continued...
可用性
- 回復性: resilience
- サーキットブレイカーの活用、失敗が一定回数を超えると切断状態になる
- fallback answer: デフォルトの処理、ドメインエキスパートと検討する
- 応答性: responsiveness, 高負荷でも応答時間が短いこと
- 早い段階でリクエストを受け付けないことを示すエラーを返すほうがいい場合がある
- バルクヘッド: 隔壁
- ロケーションを分離する方法
- サーバーを分離する方法、依存に注意する
- スレッドプールやキューを使ったコードレベルでの分離
- リアクティブ宣言: 応答性、回復性、伸縮性、メッセージ駆動
- 堅牢、耐障害、柔軟、進化性のあるシステム
- 現代のシステムに要求される性能を開発、改善しやすい設計思想
不正データの対応
- contractを使う: 些細なことで頻繁に拒否されうる
- validation: 脆弱性の一因、開発者に過大な安心感が生まれる
- ログへのデータの混入: XSSやインジェクションの危険性
public final class Name {
private final String value;
public Name(final String value) {
notBlank(value);
inclusiveBetween(2, 100, value.length(),
"Invalid length. Got: " + value.length());
matchesPattern(value, "^[a-zA-Z ]+[a-zA-Z]+$",
"Invalid name. Got: " + value);
this.value = value;
}
}
Chapter 10: merit by the way of thinking of cloud
Twelve Factor App: SaaSのための方法論
-
コードベース
-
依存関係
-
設定: configurationを外に出すことでセキュリティを向上する
- 構成ファイルに記述するのも危険
- 環境に設定する
- audit trailがインフラの責任に分離できる
- 環境の責任者のみが知りうる
- シークレットを暗号化or一時的にする必要がある
-
バックエンドサービス
-
ビルド、リリース、実行
-
プロセス: 状態を持たない独立したプロセスとしてアプリケーションを見る
- 可用性や完全性を改善できる
- デプロイとサービス稼働の権限を分ける
- 状態はDBなどに任せる
- 長いプロセスは分割して状態フラグを持たせる
-
ポートバインディング
-
並行性
-
破棄容易性
-
開発環境/本番環境の一致
-
ログ: イベントストリームとして設計する、一元管理することでセキュリティを向上させる
- ログサービスに送る
- ログへのアクセス権を管理できる
- 書き込み権限の制御による完全性の向上
- ログサービスに送る
-
管理プロセス: 単一タスクプロセスとして設計する
- システム管理機能のインターフェースは独立にデプロイされるべき
- sshなどでの操作はできることが多すぎる
-
クラウドネイティブアプリケーション: PaaS上で稼働し、elasticityで水平スケールができるよう設計されたもの
-
サービス検出と負荷分散
- クライアント側でリクエスト先を選ぶ
- サービス検出が必要になる
エンタープライズセキュリティの3つのR
- Rotate
- Repave: サーバーやアプリケーションの定期的な作り直し、ローリングデプロイ
- Repair: OSやパッケージのパッチをすぐに適用する
Chapter 11: Skipped
閑話休題なので省略
Chapter 12: apply to legacy codes
境界づられたコンテキストと意味的境界を見つけ出す
// あいまいな引数を持つメソッド
public void shipItems(int itemId, int quantity, // int
Address billing, Addres to) { // too generic class
}
public class Address {
private final String street;
private final int number;
private final String zipCode;
public Address(final String street, final int number, final String zipCode) {
this.street = street;
this.number = number;
this.zipCode = zipCode;
}
}
曖昧な引数の問題に対するアプローチ
直接的
- 一度に全ての曖昧な引数をなくす
- コードベースやチームが小さいときに機能する
- コードベースやチームが大きいと難しい
- データの質が良くないときは難しい
検出: APIを変更する前に問題を見つけ修正する
- データの質が良くないときに適している
- 不正なデータを見つけ出す
- 大規模なコードベースで複数チームで対処する場合に効果的
- 時間がかかる
- 変更が頻繁なコードベースだと対応しにくい
// 引数は変えず, 内部でドメイン・プリミティブやエンティティに変換する
// エラーはログに残す
public void shipItems(final int itemId, final int quantity,
final Address billing, final Address to) {
tryCreateItemId(itemId);
tryCreateQuantity(quantity);
tryCreateBillingAddress(billing);
tryCreateShippingAddress(to);
// ...
}
private void tryCreateItemId(final int itemId) {
try {
new ItemId(itemId);
} catch (final Exception e) {
logError("Error while creating ItemId", e);
}
}
public class ItemId {
private final int value;
public ItemId(final int value) {
isTrue(value > 0, "An item id must be greater than 0");
this.value = value;
}
public int value() {
return value;
}
}
新しいAPI: 新しいAPIを作成する
- 段階的なリファクタリング化可能
- コードベースの大小によらず効果がある
- 複数の開発者やチームで行う場合に効果がある
- データの質が良くない場合は検出アプローチと組み合わせる
public void shipItems(final ItemId itemId, // 安全なエンティティを引数に持つ
final Quantity quantity,
final BillingAddress billing,
final ShippingAddress to) {
notNull(itemId);
notNull(quantity);
notNull(billing);
notNull(to);
shipItems(itemId.value(), quantity.value(), // 処理は古いAPIに任せる
billing.address(), to.address());
}
@Deprecated
public void shipItems(int itemId, int quantity, Address billing, Address to) {
// ...
}
ログに送る文字列を検査する
- インジェクションなどのリスクの低減
- データ漏洩の防止
- Read-Onceオブジェクトの活用
- 出力しなくてはならない情報だけを指定する
過保護な実装
- 過剰なバリデーション → ドメインプリミティブを活用して回避する
- Optionalの過度な利用 → 契約の導入
class ShoppingService {
public void addToOrder(String orderId, String isbn, int qty) {
Order order = orderservice.find(orderId);
if (isbn.matches("[0-9]{9}[0-9X]")) { // バリデーション
order.addBook(isbn, qty);
}
}
}
class Order {
private Set<OrderLine> items;
public void addBook(String isbn, int qty) {
if (isbn != null && !isbn.isEmpty() && !isAlreadyInCart(isbn)) { // バリデーション
putInCartAsNew(isbn, qty);
} else { addToLine(isbn, qty); }
}
Address anAddr = ;
Optional<Address> = ;
Zip anZip = anAddr.zip();
Optional<Zip>perhapsZip = perhapsAddr.map(Address::zip);
DRY原則の誤解
- 単に繰り返されたテキストを除くことではない
- 偽陰性: 同じ考えを異なる方法で表現している
ドメイン固有の型における不十分な妥当性確認
値オブジェクトやエンティティでの確認が不十分
テストだけで十分なのか
- 異常値や極端値の場合
- 外部システムの例外のハンドリング
**不完全なドメインプリミティブ
- conceptual holeになっていない
- 金額にdoubleを使わない (2進数で表現できない)
- 必要な概念が明示的に表現されずに暗黙的に理解されている
- 1$と1€
Chapter 14: guidline at microservice
境界づけられたコンテキストを表すサービス
// crud操作を設計したAIP
public interface CustomerManagementApiV1 {
void setCustomerActivated(CustomerId id, boolean activated);
boolean isActivated(CustomerId id);
// 顧客の概念や有効の定義の管理をできていない
}
// ドメインに関する操作のみを公開したAPI
public interface CustomerManagementApiV2 {
void addLegalAgreement(CustomerId id, AgreementId agreementId);
void addConfirmedPayment(ConfirmedPaymentId confirmedPaymentId);
boolean isActivated(CustomerId id);
// 内部への直接的な処理はなくドメインへの操作のみがある
}
- APIがデータを受け取ったら、妥当性を確認し、ドメインプリミティブを作成する
- 意味の変化に注意する
- サービスを行き来する機密性の高い (sensitive) データ
- 機密性はコンテキストで変わる
- 1つのサービスの視点で考えない
- 各サービスでログを指定のフォーマットに変換してログサービスに渡す
- 暗号学的ハッシュによってデジタル署名をつけることができる
- サービスのバージョンを記録するようにする
- インスタンスIDを記録する
- トランザクションを識別できるようにする
機密性を確保するために、ログを見るためのインターフェースを作る
- ログデータの分類が必要
- システムの振る舞いを知る必要のあるユーザーに限る
- e.g. ドメインに基づくログAPIを使う
- 監査用のログデータ
- 振る舞いに関するログデータ
- エラーに関するログデータ
- 同じサービスに異なるログを保存する場合
- 実装が楽
- 見るユーザーによって制限する
- 安全性に懸念がある
- 異なる格納先を使う場合
- 安全性に優れる
- traceabilityを向上できる
- ライフサイクルを個別化できる
public Result cancel(final BookingId bookingId, final User user) {
notNull(bookingId);
nutNull(user);
logger.cancelBooking(bookingId, user); // リクエストを受けたときのログ
final Result result = bookingsRepository.cancel(bookingId);
if (result.isBookingCanceled()) {
logger.bookingCanceled(bookingId, user); // 成功時のログ
} else {
logger.bookingCancellationFailed(bookingId, result, user); // 失敗時のログ
}
return result;
}
private final Logger logger = ...
public void bookingCancellationFailed(final BookingId id, final Result result, final User user) {
notNull(id);
notNull(result);
notNull(user);
logger.log(auditData(id, result, user)); // 監査用のログデータを作成して出力
logger.log(behaviorData(result)); // 振る舞いに関するログデータ
if (result.isError()) {
logger.log(errorData(result)); // エラーに関するログデータ
}
}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
private String auditData(final BookingId bookingId, final Result result, final User user) {
final Map<String, String> data = new HashMap<>();
data.put("category", "audit"); // 監査用である
data.put("message", "Failed to cancel booking");
data.put("bookingId", bookingId.value());
data.put("username", user.name());
data.put("status", result.status());
return asJson(data, "Failure translating audit data into JSON");
}
// オブジェクトをJSONに変換する
private final ObjectMapper objectMapper = ...
// JSONへの変換に失敗した際のメッセージ
private String asJson(final Map<String, String> data, final String errorMessage) {
try {
return objectMapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
return String.format("{\"failure\":\"%s\"}", errorMessage);
}
Chapter 14: don't forget security
- セキュリティを意識したコードレビュー
- 観点例
- WEBアプリのデータを送受信する際、適切なエンコードされているか
- セキュリティに関するHTTPヘッダーが適切に使われているか
- XSS攻撃の対策として何が行われているか
- 不変条件はドメインプリミティブできちんと確認されているか
- 自動化されたセキュリティのテストがデリバリパイプラインに組み込まれているか
- システムで利用されるパスワードはどのくらいの頻度で変更されるか
- システムで利用する証明書はどのくらいの頻度で変更されるか
- 機密性の高いデータが間違ってログに書き出されないような手段は
- パスワードはどのように保護、格納されているか
- データを保護するのに適切な暗号方式が使われているか
- SQLがパラメータ化されているか
- セキュリティ観点の監視が行われているか
- 観点例
- 積み重なった技術の中に潜む危険性
- アプリケーションの数が多い場合、脆弱性を監視、管理するには戦略が必要
- 情報を集約するツールを使う
- penetration testの定期的な実施
- セキュリティの欠陥は一般のバグとして対処できる
- テストを学習の機会とする
- 適切な頻度は状況による
- コンテキスト駆動テスト
- テストの価値はコンテキストによる
- ベストプラクティスは存在しない
- プロジェクト参加者が最も重要な要素
- プロジェクトは想定できない方向に進む
- プロダクトによっても問題が解決されないのであれば、それは機能していない
- 優れたテストはソフトウェアを理解するための知的プロセス
- バグ報奨金制度: 結構大変
- 最新のセキュリティ侵害と攻撃経路の把握
- OWASP top 10を見る
- セキュリティ問題が発生した時の対応とチームの役割
- 開発チームの作業に自然と含まれるのが望ましい
- インシデントハンドリング: インシデントが起こった時に実施すること、被害を抑える、優先度が高い
- 問題解決: インシデントの原因に対処すること、通常の優先度
- Wolffの法則: 医学、骨が力学的負荷で強固な構造になること
- チェックリスト
- レベル3「同様のバグも見つける」の対応をすると反脆弱性を得られる