refactor: 문서 개선 중
This commit is contained in:
@@ -721,16 +721,20 @@ public InboxCleanupJob inboxCleanupJob(...)
|
||||
`JdbcInboxRepository`)을 **어떤 자동설정도 만들지 않는다.** 19 main 파일 / 2,818 LOC가 전부 조용히
|
||||
비어 있다.
|
||||
|
||||
**실패가 특히 조용하다.** Spring은 조건부 bean이 조건을 만족하지 못하는 것을 오류로 보고하지
|
||||
않는다. 즉 **"outbox가 꺼져 있음"과 "outbox가 조립될 수 없음"이 런타임에서 구별되지 않는다.**
|
||||
**실패가 애플리케이션 오류나 health failure로 자동 승격되지는 않는다.** 따라서 기능을 호출하지 않는 한
|
||||
"outbox가 꺼져 있음"과 "outbox bean이 조건 불일치로 생성되지 않음"이 제품 동작에서는 비슷하게 보일 수 있다.
|
||||
다만 condition evaluation evidence가 사라지는 것은 아니다. Spring Boot의 `ConditionEvaluationReport`와,
|
||||
endpoint가 노출된 경우 `/actuator/conditions`에서 positive/negative match와 이유를 확인할 수 있다.
|
||||
같은 starter가 `MessageCodecRegistry`에는 `@ConditionalOnMissingBean` 기본 구현을 제공했다는 점이
|
||||
이것을 결함으로 만든다.
|
||||
이 조립 차이를 검토할 이유다.
|
||||
|
||||
**마이그레이션 스트림을 적용하는 곳이 없고, 적용하려는 순간 버전이 충돌한다** (`19` §7.2).
|
||||
|
||||
합성 루트의 Flyway 기본 위치는 `PostgreSqlPersistenceConfig:115`의
|
||||
`classpath:db/migration/postgresql`이고, `db/migration/messaging`을 이름으로 부르는 main 코드가
|
||||
저장소 전체에 **0건**이다. 그리고 두 leaf가 같은 리소스 디렉터리에 각자 번호를 매긴다:
|
||||
`classpath:db/migration/postgresql`이다. searched direct reference 기준으로 `db/migration/messaging`을
|
||||
이름으로 지정하는 main 코드는 찾지 못했다. 이 결과는 직접 지정 코드가 검색되지 않았다는 뜻이며,
|
||||
외부 설정·resource scanning·reflection·framework convention까지 포함해 runtime 사용이 없음을 단독으로 증명하지는 않는다.
|
||||
두 leaf가 같은 리소스 디렉터리에 각자 번호를 매긴다는 사실은 별개로 유지된다:
|
||||
|
||||
```
|
||||
messaging-inbox-jdbc-postgresql V2__messaging_inbox.sql (CREATE TABLE)
|
||||
@@ -7397,6 +7401,8 @@ Evidence: `evidence/raw/096-experimental-gate-reachability.txt`, `099-experiment
|
||||
|
||||
`RlsPolicyVerifier.requireEnforced(runtimeDataSource, tenantScopedTables)`의 이름과 Javadoc은 caller가 지정한 tenant-scoped table들이 실제로 RLS에 의해 보호되는지 증명하는 contract다. 구현은 runtime role의 `BYPASSRLS`를 확인하고, `current_schema()`의 실제 table들을 순회하면서 이름이 `tenantScopedTables`에 포함된 row만 검사한다.
|
||||
|
||||
여기서 PostgreSQL 의미를 분리해서 읽어야 한다. RLS가 꺼져 있으면 policy가 적용되지 않는다. RLS가 켜져 있고 현재 role에 적용 가능한 policy가 없으면 일반 role에는 **default deny**가 적용된다. superuser와 `BYPASSRLS` role은 RLS를 우회한다. table owner도 기본적으로 우회하지만 `FORCE ROW LEVEL SECURITY`를 켜면 owner는 policy 대상이 된다. `FORCE`가 superuser나 `BYPASSRLS`의 우회를 없애는 것은 아니다. 따라서 이 값들을 항상 동시에 참이어야 하는 ‘세 전제’로 묶지 않는다.
|
||||
|
||||
문제는 반대 방향 검증이 없다는 것이다. 즉 caller가 요구한 table 이름이 실제 catalog 결과에 **한 번도 등장하지 않아도** 성공한다.
|
||||
|
||||
```text
|
||||
@@ -11438,7 +11444,7 @@ PROBE NUL byte then <html> -> ACCEPT / NO_SCRIPTABLE_CONTENT ←
|
||||
PROBE plain text -> ACCEPT / NO_SCRIPTABLE_CONTENT
|
||||
```
|
||||
|
||||
세 가지가 통과한다. `String.stripLeading()`은 `Character.isWhitespace`만 제거하므로 **UTF-8 BOM(U+FEFF)도 NUL도 지우지 않고**, 선행 HTML 주석은 어떤 마커로도 시작하지 않는다. 셋 다 브라우저는 HTML로 렌더링한다 — BOM 접두 HTML은 이국적인 우회가 아니라 여러 편집기의 기본 출력이다.
|
||||
세 가지가 detector를 통과한다. `String.stripLeading()`은 `Character.isWhitespace`만 제거하므로 **UTF-8 BOM(U+FEFF)도 NUL도 지우지 않고**, 선행 HTML 주석은 어떤 마커로도 시작하지 않는다. 이 probe가 증명한 범위는 detector bypass까지다. 실제 대상 브라우저가 각 입력을 실행 가능한 콘텐츠로 해석하는지는 이번 evidence에서 확인하지 않았다.
|
||||
|
||||
형제 검증기와의 대비가 판정을 굳힌다. `MediaTypeVerifier`는 매직바이트를 접두사 **시작**에서 비교하는데, 그것은 시그니처의 정의가 파일 선두이므로 옳다. scriptable 마커는 시그니처가 아니라 **브라우저가 스니핑하는 패턴**이고, 브라우저는 선두 고정 매칭을 하지 않는다. 같은 "접두사 시작 비교"가 한쪽에서는 정확하고 다른 쪽에서는 우회 가능하다.
|
||||
|
||||
@@ -11485,7 +11491,7 @@ PROBE plain text -> ACCEPT / NO_SCRIPTABLE_CONTENT
|
||||
| 우선순위 | finding | reachability |
|
||||
|---|---|---|
|
||||
| **P2** | README:105 "No setting or bean for those capabilities is exposed"가 audit·health·reaping·quota 네 능력에 대해 사실과 다르다 — 8개 port 구현과 app-bootstrap의 8개 bean으로 확정 | 이 문단으로 능력 유무를 판단하는 독자 |
|
||||
| **P2** | `ScriptableContentPolicy`가 마커를 접두사 **시작**에서만 찾아, UTF-8 BOM·NUL·선행 HTML 주석이 붙은 실행 가능 콘텐츠를 ACCEPT한다 (실행 probe 3건) | `inlineSafeProfile=false`이고 claimed 타입을 선언하지 않는 업로드 |
|
||||
| **P2** | `ScriptableContentPolicy`가 마커를 접두사 **시작**에서만 찾아, UTF-8 BOM·NUL·선행 HTML 주석이 붙은 입력을 ACCEPT한다 (detector probe 3건). 실제 브라우저 실행 가능성은 별도 검증하지 않음 | `inlineSafeProfile=false`이고 claimed 타입을 선언하지 않는 업로드 |
|
||||
| **P3/기록** | 실패 분류가 예외 메시지 텍스트("stale file handle", "timed out", "No space left on device")에 의존한다 — 문구가 달라지면 보수적 기본값으로 떨어지므로 안전한 방향 | 로케일/JDK 판본이 다른 배포 |
|
||||
|
||||
#### 46. Sub-scope 05 완료 조건
|
||||
@@ -13467,7 +13473,7 @@ RedisEphemeralFanoutAdapter implements EphemeralFanoutPort
|
||||
> `CommandPolicyGuard`: "**The single admission point every command passes through.**"
|
||||
> `RedisCommandGateway`: "Policy, permits, budgets, timeouts, and observability are not this interface's concern: **everything routed through it has already passed `CommandPolicyGuard`**."
|
||||
|
||||
의미 어댑터 다섯은 그 전제를 만족하지 않는다(`165-...` §8.1).
|
||||
이 문장은 현재 runtime 전체의 사실이 아니라 **의도된 guarded command path의 계약**으로 읽어야 한다. 의미 어댑터 다섯은 그 전제를 만족하지 않는다(`165-...` §8.1). 따라서 이후 admission 단계 설명도 guard를 통과하는 경로에 한정한다.
|
||||
|
||||
- `SyncRedisCommandExecutor`·`ReactiveRedisCommandExecutor`·`CommandPolicyGuard`·`CommandRequest`를 참조하는 파일 **0**(exit=1)
|
||||
- 타입 있는 API(`RedisValueOperations`·`RedisHashOperations`·`RedisKeyOperations`·`RedisOperations`)를 참조하는 파일 **0**(exit=1)
|
||||
@@ -17555,11 +17561,11 @@ private ExternalRequestContext externalRequest(ServerHttpRequest request) {
|
||||
|
||||
**권고** — 하나를 남긴다. `RequestLoggingFilter`가 `WebMvcRequestIdFilter`가 요청 속성에 넣은 값을 읽게 하면(`WebMvcRequestIdFilter.requestId(request)`가 이미 그 접근자다) 정책이 한 곳에 남고 MDC·로그·응답 헤더가 일치한다.
|
||||
|
||||
##### 32.2 P2 — forwarded 헤더 신뢰 판정이 Nginx 설정에만 있고, 그것을 위해 쓴 Java 정책 421 LOC은 배선되지 않는다
|
||||
##### 32.2 P2 — 테스트 Nginx 설정은 forwarded 헤더를 교체하지만 운영 trust boundary와 Java 정책 wiring은 미확인이다
|
||||
|
||||
`server.forward-headers-strategy=framework`(기본값)에서 Spring이 `X-Forwarded-Proto`·`X-Forwarded-Host`·`X-Forwarded-Port`·`X-Forwarded-Prefix`를 **보낸 피어가 누구든** 반영한다. 그 값이 `request.getURI()`를 바꾸고, 그것이 `ExternalRequestContext`가 되고(§31.4), 그것으로 `Location` 헤더와 페이지네이션 링크가 만들어진다.
|
||||
|
||||
스푸핑을 막는 것은 `nginxProxyTest` 레인이 증명하는 **Nginx 설정**이다:
|
||||
`nginxProxyTest` 레인에서는 다음 **Nginx 설정**이 클라이언트가 보낸 forwarded 헤더를 교체한다:
|
||||
|
||||
```
|
||||
NginxProxyContractIT:63 attackerCannotOverrideForwardedHost() X-Forwarded-Host: evil.example
|
||||
@@ -17569,15 +17575,15 @@ NginxProxyContractIT:143 clientCannotInjectAPrefix()
|
||||
// "X-Forwarded-Prefix is set per location, so a client's value is replaced."
|
||||
```
|
||||
|
||||
이 보증의 근거는 `nginxProxyTest/resources/nginx/proxy_headers.conf`가 location마다 헤더를 **덮어쓴다**는 사실이다. 애플리케이션은 검사하지 않는다.
|
||||
이 테스트 레인의 보증 근거는 `nginxProxyTest/resources/nginx/proxy_headers.conf`가 location마다 헤더를 **덮어쓴다**는 사실이다. 이 결과만으로 실제 운영 배포가 같은 설정을 사용한다고 보거나, 모든 운영 경로에서 애플리케이션 검사가 없다고 단정하지 않는다.
|
||||
|
||||
`TrustedProxyPolicy`(161줄, CIDR 기반 피어 허용목록)가 애플리케이션 쪽 검사를 위해 존재하고, 프로덕션에서 생성되지 않는다. testkit의 `ProxyFixtureController:53`이 `TrustedProxyPolicy.of("10.0.0.0/8", …)`를 직접 만들어 픽스처에 붙인다 — SS4·SS5와 같은 형태다.
|
||||
`TrustedProxyPolicy`(161줄, CIDR 기반 피어 허용목록)가 애플리케이션 쪽 검사를 위해 존재한다. searched direct reference와 확인한 자동설정 경로에서는 production construction을 찾지 못했고, testkit의 `ProxyFixtureController:53`은 `TrustedProxyPolicy.of("10.0.0.0/8", …)`를 직접 만들어 픽스처에 붙인다. reflection·framework lifecycle·외부 조립까지 포함한 전체 runtime wiring 부재는 이번 evidence로 확정하지 않았다.
|
||||
|
||||
**실패 시나리오** — 배포가 그 Nginx 설정을 쓰지 않거나(다른 인그레스, 서비스 메시, k8s 내부에서 파드 IP로 직접 도달), 인그레스를 우회하는 경로가 하나라도 있으면, 클라이언트가 `X-Forwarded-Host: evil.example`을 보내 그 요청이 만드는 모든 절대 URL을 자기 도메인으로 돌린다. 비밀번호 재설정 링크나 `Location` 헤더가 그 URL을 담으면 그대로 피싱 벡터가 된다.
|
||||
**조건부 실패 시나리오** — 운영 배포가 forwarded 헤더를 신뢰하면서도 앞단에서 값을 교체·검증하지 않는 경로가 있다면, 클라이언트가 `X-Forwarded-Host: evil.example` 같은 값을 주입해 절대 URL 생성에 영향을 줄 수 있다. 이번 검증은 그러한 운영 경로가 실제로 존재하는지까지 확인하지 않았다.
|
||||
|
||||
**이것을 방어로 쓰는 것 자체는 정당하다** — 인그레스에서 덮어쓰는 것이 표준 관행이다. 기록하는 것은 두 가지다: (1) 그 의존이 코드나 문서에 명시돼 있지 않고 레인의 `.conf` 파일에만 있다, (2) 애플리케이션 쪽 이중 방어로 쓰라고 421줄을 작성해 두고 연결하지 않았다.
|
||||
**인그레스에서 forwarded 헤더를 authoritative value로 교체하는 설계 자체는 가능하다.** 이번 evidence가 확인한 것은 테스트 `.conf`의 교체 동작과 Java trust-policy 코드의 존재다. 실제 운영이 이 설정에 의존하는지, Java 정책이 운영 lifecycle에서 정말 연결되지 않는지는 추가 조립·배포 evidence가 필요하다.
|
||||
|
||||
**권고** — `TrustedProxyPolicy`를 `forward-headers-strategy` 앞단에 배선하거나(피어가 목록 밖이면 forwarded 헤더를 버린다), 최소한 README에 "이 플랫폼은 인그레스가 `X-Forwarded-*`를 덮어쓴다고 전제한다"를 명시하고 `proxy` 패키지를 제거한다. 지금 상태는 그 전제를 아무 데도 적지 않은 채 그것을 대체할 코드를 갖고 있다.
|
||||
**권고** — 운영 trust boundary를 먼저 확정한다. 인그레스가 `X-Forwarded-*`를 authoritative value로 교체하는 구조라면 그 전제를 운영 문서와 계약 테스트에 명시한다. 애플리케이션에서도 피어 신뢰를 검증하려는 설계라면 `TrustedProxyPolicy`의 실제 lifecycle wiring을 확인하고 빠진 경로를 연결한다.
|
||||
|
||||
##### 32.3 P3/기록 — `ExternalRequestContext.prefix`가 항상 빈 문자열이고 `WebAuditPublisher`는 참조 0이다
|
||||
|
||||
@@ -18040,7 +18046,7 @@ web 쪽이 구조적으로 우월하다. `build.gradle`이 그 이유를 적는
|
||||
| P2 | 24.1 | 배선된 `CacheControlFilter`의 `no-store`가 배선된 조건부 읽기(ETag/304)를 무력화하고, 조정용 `cache` 패키지 310 LOC은 참조 0 |
|
||||
| P2 | 28.1 | `maxArrayElements`가 선언만 되고 강제되지 않으며 바이트 예산 백스톱(§16.1)도 없다 |
|
||||
| P2 | 32.1 | 요청 식별자를 클라이언트가 고를 수 없다는 정책이, 뒤에 도는 다른 배선 필터에 의해 뒤집힌다 |
|
||||
| P2 | 32.2 | forwarded 헤더 신뢰 판정이 Nginx 설정에만 있고 Java 정책 421 LOC은 미배선 |
|
||||
| P2 | 32.2 | 테스트 Nginx 설정은 forwarded 헤더를 교체한다. 운영 trust boundary와 Java 정책의 전체 lifecycle wiring은 이번 evidence로 확정하지 못함 |
|
||||
| P2 | 36.1 | 선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다 |
|
||||
| P3 ×9 | 8.2 · 20.3 · 24.3 · 25.3 · 25.4 · 36.2 · 44.1 · 44.2 · 28.3 외 | 죽은 메서드 · 구분자 기반 지문 · 미강제 한도 · 이름 불일치 · 참조 0인 138줄 · 실행되지 않는 시작 검증 등 |
|
||||
|
||||
@@ -22425,9 +22431,9 @@ they write inside the application's own transaction, against the application's o
|
||||
configuration.locations("classpath:db/migration/postgresql");
|
||||
```
|
||||
|
||||
조건부 스트림은 각자 자기 위치와 history table을 갖는다 — `NotificationSchemaStream.LOCATION = "classpath:db/migration/jpa/notification-platform"`, fileserver 스트림 등. **`db/migration/messaging`을 이름으로 부르는 main 코드는 저장소 전체에 0건이다.** 참조는 세 개의 IT(`InboxPostgresIT`, `OutboxPostgresIT`, `AdminOperationJournalPostgresIT`)가 자기 테스트 컨테이너에 직접 적용할 때뿐이다.
|
||||
조건부 스트림은 각자 자기 위치와 history table을 갖는다 — `NotificationSchemaStream.LOCATION = "classpath:db/migration/jpa/notification-platform"`, fileserver 스트림 등. searched direct reference 기준으로 **`db/migration/messaging`을 이름으로 지정하는 main 코드는 찾지 못했다.** 검색된 참조는 세 개의 IT(`InboxPostgresIT`, `OutboxPostgresIT`, `AdminOperationJournalPostgresIT`)가 자기 테스트 컨테이너에 직접 적용하는 경로다.
|
||||
|
||||
즉 `messaging_outbox` · `messaging_inbox` · admin operation journal 테이블은 **출하 배포 어디에서도 생성되지 않는다.** §7.1과 합치면 일관은 있다 — repository bean이 없으니 테이블도 필요 없다. 그러나 `persistence-jpa` leaf가 같은 모양의 결함을 세 번 고치고 그 이력을 javadoc에 남겨 두었다:
|
||||
이 정적 검색만으로 외부 설정·resource scanning·reflection·framework convention을 모두 배제할 수는 없다. 따라서 여기서 확정할 수 있는 것은 출하 코드 안의 직접 wiring을 찾지 못했다는 범위까지다. repository bean이 현재 확인한 자동설정 경로에서 만들어지지 않는다는 §7.1 결과와 함께 보면 runtime assembly가 닫혀 있지 않다는 신호는 강하지만, ‘어떤 출하 배포에서도 테이블이 생성되지 않는다’고 일반화하지 않는다. 그러나 `persistence-jpa` leaf가 같은 모양의 결함을 세 번 고치고 그 이력을 javadoc에 남겨 두었다:
|
||||
|
||||
> "`PostgreSqlSameStoreInboxAdapter` ... its tables live only in `db/migration/jpa/inbox`. **The bean existed, its tables did not**, and the failure arrived either at ..."
|
||||
> (같은 문장이 `PostgreSqlImmutableOutboxAppendAdapter`, `PostgreSqlPollingDeliveryAdapter`에도 있다)
|
||||
@@ -22444,7 +22450,7 @@ messaging-outbox-jdbc-postgresql : V1__messaging_outbox.sql
|
||||
V4__messaging_outbox_canonical_metadata.sql
|
||||
```
|
||||
|
||||
**`V2`가 두 개다.** 두 jar가 한 classpath에 있고 Flyway가 `classpath:db/migration/messaging`을 스캔하면 "Found more than one migration with version 2"로 실패한다. 지금 실패하지 않는 유일한 이유는 (a) — 아무도 그 위치를 Flyway에 주지 않기 때문이다.
|
||||
**`V2`가 두 개다.** 두 jar가 한 classpath에 있고 Flyway가 `classpath:db/migration/messaging`을 같은 location으로 스캔하면 duplicate version 오류가 된다. 현재 정적 검색에서는 그 location을 직접 지정하는 main 코드를 찾지 못했지만, 이것을 현재 실패하지 않는 ‘유일한 이유’로 단정하지 않는다. framework/external configuration 경로는 이번 정적 검색으로 닫지 못했다.
|
||||
|
||||
각 leaf의 IT는 자기 jar의 리소스만 보므로 이 충돌을 재현하지 못한다 — `InboxPostgresIT:199`는 `V2__messaging_inbox.sql`을 파일명으로 직접 읽고, `OutboxPostgresIT:249`는 자기 디렉터리를 나열한다. **두 leaf를 한 classpath에 올린 상태를 검증하는 테스트가 없다.**
|
||||
|
||||
@@ -23056,7 +23062,7 @@ grpc-spring-boot-starter/src/test/.../GrpcPlatformStartupValidatorTest.java (1
|
||||
grpc-spring-boot-starter/src/main/.../GrpcPlatformStartupValidator.java (선언 자신)
|
||||
```
|
||||
|
||||
**main 참조 0.** 클래스는 `final` + `private` 생성자 + static 메서드(`violations(...)`, `requireValid(...)`)이므로 bean이 될 수도 없다 — 누군가 `requireValid`를 호출해야 하고, 호출하는 곳이 없다.
|
||||
searched direct reference 기준으로 production caller를 찾지 못했다. 클래스가 `final` + `private` 생성자 + static 메서드(`violations(...)`, `requireValid(...)`)인 것도 자동 bean 등록 경로가 아니라는 강한 신호다. 다만 direct reference 0만으로 reflection·framework discovery까지 전부 배제했다고 말하지 않는다. 실제 실행 여부는 assembly/lifecycle 경로와 boot evidence를 함께 확인해야 한다.
|
||||
|
||||
**실행되지 않는 규칙이 13개다.** validator 본문을 읽어 전수 확인했다:
|
||||
|
||||
@@ -23076,7 +23082,7 @@ grpc-spring-boot-starter/src/main/.../GrpcPlatformStartupValidator.java (
|
||||
|
||||
CLAUDE.md가 인용한 "streaming method가 Stable catalog에 등록되면 거부"는 methods 그룹의 네 번째 규칙(`!policy.rpcType().stable()`)이고, §2.2의 runtime 강제는 advanced isolation 그룹의 유일한 규칙이다. **둘 다 실행되지 않는다.**
|
||||
|
||||
이 형태는 이 저장소에서 네 번째다 — 모듈 14 §44.2(`WebPlatformStartupValidator`), 모듈 17 §4.1(`WebSocketPlatformStartupValidator`), 모듈 19 §3.5(`KafkaTransactionProfileValidator`), 그리고 여기. 그리고 모듈 18에서 확립한 규칙이 다시 성립한다 — **시작 검증기가 도는지 여부는 그 능력에 자동설정 루트가 있는지와 일치한다**. 여기서는 루트가 **있는데도** 검증기를 부르지 않는 첫 사례다.
|
||||
이 형태는 이 저장소에서 반복해서 나타난다 — 모듈 14 §44.2(`WebPlatformStartupValidator`), 모듈 17 §4.1(`WebSocketPlatformStartupValidator`), 모듈 19 §3.5(`KafkaTransactionProfileValidator`), 그리고 여기다. 여기서 일반화할 수 있는 규칙은 ‘auto-configuration root 존재 여부와 실행 여부가 일치한다’가 아니다. **시작 검증기의 실행 여부는 direct caller뿐 아니라 `@Bean`/component scan/auto-configuration, lifecycle callback, event/post processor, framework discovery와 실제 boot evidence까지 따라가서 확인한다.** 이 사례에서는 확인한 auto-configuration이 검증기를 직접 부르지 않는다는 사실까지 확정했다.
|
||||
|
||||
**채택 시점 실패 시나리오.** 팀이 `runtime_memberships`에 런타임을 추가하고 `ca-skeleton.grpc.platform.enabled=true`로 켠다. Stable catalog에 client-streaming 메서드를 하나 등록한다(Stable 범위 밖이라는 것을 모른 채). 부팅은 성공한다. 그 메서드는 Stable이 보장하지 않는 경로로 실행되고, `grpc-advanced-streaming`의 세션·중복제거·체크포인트 기계는 조립돼 있지 않다. 거부했어야 할 검증기는 존재하고, 테스트도 12개 통과하며, 호출되지 않는다.
|
||||
|
||||
@@ -24404,6 +24410,8 @@ private static void appendField(StringBuilder canonical, String value) {
|
||||
*/
|
||||
```
|
||||
|
||||
위 javadoc의 ‘one byte at a time’ 표현은 timing 공격의 위험을 설명하려는 문구지만, Java API가 보장하는 성질보다 강하게 읽지 않는다. 핵심은 입력 내용이나 common prefix에 따라 일찍 끝나는 비교를 피하고, JDK `MessageDigest.isEqual`이 문서화한 comparison timing property를 사용하는 것이다.
|
||||
|
||||
세 가지가 코드로 지켜진다.
|
||||
|
||||
```java
|
||||
|
||||
Reference in New Issue
Block a user