@nodefony/security 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +182 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +151 -0
- package/dist/nodefony/command/security-secrets.js +158 -0
- package/dist/nodefony/command/security-token.js +335 -0
- package/dist/nodefony/command/security-user-add.js +131 -0
- package/dist/nodefony/command/security-user-delete.js +102 -0
- package/dist/nodefony/command/security-user-list.js +77 -0
- package/dist/nodefony/config/config.js +366 -0
- package/dist/nodefony/config/defineModuleConfig.js +35 -0
- package/dist/nodefony/contracts/IAccessVoter.js +13 -0
- package/dist/nodefony/contracts/IApiKey.js +1 -0
- package/dist/nodefony/contracts/IAuditEvent.js +1 -0
- package/dist/nodefony/contracts/IAuditStore.js +1 -0
- package/dist/nodefony/contracts/IAuthenticator.js +1 -0
- package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
- package/dist/nodefony/contracts/IFirewall.js +1 -0
- package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
- package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
- package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
- package/dist/nodefony/contracts/ISecuredArea.js +1 -0
- package/dist/nodefony/contracts/IToken.js +1 -0
- package/dist/nodefony/contracts/ITokenStore.js +1 -0
- package/dist/nodefony/contracts/ITotpSecret.js +1 -0
- package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
- package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
- package/dist/nodefony/contracts/IWebhookStore.js +1 -0
- package/dist/nodefony/contracts/index.js +2 -0
- package/dist/nodefony/errors/AccessDeniedError.js +14 -0
- package/dist/nodefony/errors/ApiKeyError.js +21 -0
- package/dist/nodefony/errors/AuthenticationError.js +14 -0
- package/dist/nodefony/errors/CsrfError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +39 -0
- package/dist/nodefony/errors/SsrfError.js +17 -0
- package/dist/nodefony/errors/ThrottledError.js +21 -0
- package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
- package/dist/nodefony/errors/WebAuthnError.js +21 -0
- package/dist/nodefony/errors/index.js +9 -0
- package/dist/nodefony/service/accessTokenVerifier.js +77 -0
- package/dist/nodefony/service/apiKeys.js +310 -0
- package/dist/nodefony/service/auditService.js +145 -0
- package/dist/nodefony/service/authFlow.js +332 -0
- package/dist/nodefony/service/authorization.js +95 -0
- package/dist/nodefony/service/cors.js +81 -0
- package/dist/nodefony/service/csrf.js +97 -0
- package/dist/nodefony/service/firewall.js +699 -0
- package/dist/nodefony/service/oauth2.js +153 -0
- package/dist/nodefony/service/securityHeaders.js +80 -0
- package/dist/nodefony/service/tokenService.js +486 -0
- package/dist/nodefony/service/totp.js +209 -0
- package/dist/nodefony/service/webAuthn.js +343 -0
- package/dist/nodefony/service/webhooks.js +539 -0
- package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
- package/dist/nodefony/src/SecuredArea.js +51 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
- package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
- package/dist/nodefony/src/admin/adminAudit.js +37 -0
- package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
- package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
- package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
- package/dist/nodefony/src/audit/auditBridge.js +82 -0
- package/dist/nodefony/src/audit/auditFilters.js +60 -0
- package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
- package/dist/nodefony/src/audit/readAuditContext.js +24 -0
- package/dist/nodefony/src/audit/recordAudit.js +16 -0
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
- package/dist/nodefony/src/authenticator/bearer.js +2 -0
- package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
- package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
- package/dist/nodefony/src/crypto/secretCipher.js +79 -0
- package/dist/nodefony/src/csp.js +54 -0
- package/dist/nodefony/src/csrfToken.js +65 -0
- package/dist/nodefony/src/net/ssrfGuard.js +130 -0
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
- package/dist/nodefony/src/oauth/providers/github.js +65 -0
- package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
- package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
- package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
- package/dist/nodefony/src/sessionIdentity.js +35 -0
- package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
- package/dist/nodefony/src/token/AnonymousToken.js +40 -0
- package/dist/nodefony/src/token/JwtKeystore.js +160 -0
- package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
- package/dist/nodefony/src/token/UserToken.js +67 -0
- package/dist/nodefony/src/token/jwtRuntime.js +19 -0
- package/dist/nodefony/src/token/secretFile.js +134 -0
- package/dist/nodefony/src/token/tokenCriteria.js +35 -0
- package/dist/nodefony/src/token/tokenFilters.js +72 -0
- package/dist/nodefony/src/token/tokenSort.js +40 -0
- package/dist/nodefony/src/token/tokenStatus.js +35 -0
- package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
- package/dist/nodefony/src/totp/totpCipher.js +30 -0
- package/dist/nodefony/src/totp/totpCrypto.js +226 -0
- package/dist/nodefony/src/totp/totpOperations.js +129 -0
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
- package/dist/nodefony/src/voter/RoleVoter.js +32 -0
- package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
- package/dist/nodefony/src/voter/voterRegistry.js +20 -0
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
- package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
- package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
- package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
- package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
- package/dist/nodefony/src/webhook/webhookSort.js +48 -0
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
- package/dist/types/nodefony/command/security-token.d.ts +44 -0
- package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
- package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
- package/dist/types/nodefony/config/config.d.ts +295 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
- package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
- package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
- package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
- package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
- package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
- package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
- package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
- package/dist/types/nodefony/contracts/index.d.ts +9 -0
- package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
- package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
- package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
- package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
- package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
- package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
- package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +8 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
- package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
- package/dist/types/nodefony/service/auditService.d.ts +30 -0
- package/dist/types/nodefony/service/authFlow.d.ts +123 -0
- package/dist/types/nodefony/service/authorization.d.ts +33 -0
- package/dist/types/nodefony/service/cors.d.ts +48 -0
- package/dist/types/nodefony/service/csrf.d.ts +57 -0
- package/dist/types/nodefony/service/firewall.d.ts +148 -0
- package/dist/types/nodefony/service/oauth2.d.ts +66 -0
- package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
- package/dist/types/nodefony/service/tokenService.d.ts +103 -0
- package/dist/types/nodefony/service/totp.d.ts +58 -0
- package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
- package/dist/types/nodefony/service/webhooks.d.ts +160 -0
- package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
- package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
- package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
- package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
- package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
- package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
- package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
- package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
- package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
- package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
- package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
- package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
- package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
- package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
- package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
- package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
- package/dist/types/nodefony/src/csp.d.ts +39 -0
- package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
- package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
- package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
- package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
- package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
- package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
- package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
- package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
- package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
- package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
- package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
- package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
- package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
- package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
- package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
- package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
- package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
- package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
- package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
- package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
- package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
- package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
- package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
- package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
- package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
- package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
- package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
- package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
- package/docs/api-keys.md +691 -0
- package/docs/audit.md +751 -0
- package/docs/authenticators.md +487 -0
- package/docs/authorization.md +497 -0
- package/docs/cors.md +497 -0
- package/docs/csrf.md +392 -0
- package/docs/external-jwt.md +181 -0
- package/docs/firewall.md +546 -0
- package/docs/headers.md +616 -0
- package/docs/index.md +207 -0
- package/docs/lexique.md +190 -0
- package/docs/oauth2.md +575 -0
- package/docs/obtenir-un-jeton.md +225 -0
- package/docs/tokens.md +520 -0
- package/docs/totp.md +804 -0
- package/docs/webauthn.md +733 -0
- package/docs/webhooks.md +1016 -0
- package/package.json +83 -0
package/docs/cors.md
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "CORS — partage cross-origin contrôlé"
|
|
3
|
+
navTitle: CORS
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/security"
|
|
6
|
+
topic: cors
|
|
7
|
+
coverageModule: security
|
|
8
|
+
coverageFiles: "cors.ts"
|
|
9
|
+
section: "Sécurité"
|
|
10
|
+
audience: [developer]
|
|
11
|
+
tags:
|
|
12
|
+
[
|
|
13
|
+
security,
|
|
14
|
+
cors,
|
|
15
|
+
preflight,
|
|
16
|
+
access-control,
|
|
17
|
+
vary,
|
|
18
|
+
credentials,
|
|
19
|
+
cswsh,
|
|
20
|
+
owasp,
|
|
21
|
+
fetch-standard,
|
|
22
|
+
]
|
|
23
|
+
version: "doc"
|
|
24
|
+
status: stable
|
|
25
|
+
updated: 2026-07-19
|
|
26
|
+
source: "src/packages/@nodefony/security/docs/cors.md"
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# CORS — autoriser (ou non) les appels cross-origin
|
|
30
|
+
|
|
31
|
+
> Par défaut un navigateur **interdit** à `https://app.example.com` de lire la réponse de
|
|
32
|
+
> `https://api.example.com` (Same-Origin Policy). CORS est le protocole qui **assouplit** cette règle,
|
|
33
|
+
> côté serveur, en posant des en-têtes `Access-Control-*`. Ce n'est pas une défense — c'est l'inverse :
|
|
34
|
+
> un moyen d'**ouvrir** des portes précises sans tout ouvrir. Nodefony en fait une politique **pure**,
|
|
35
|
+
> **fail-safe** et **verrouillée au boot**. Ancré sur
|
|
36
|
+
> `src/packages/@nodefony/security/nodefony/service/cors.ts`.
|
|
37
|
+
|
|
38
|
+
📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **CORS**
|
|
39
|
+
|
|
40
|
+
## 🧠 Le modèle mental — deux moments, une allowlist
|
|
41
|
+
|
|
42
|
+
```mermaid
|
|
43
|
+
flowchart TD
|
|
44
|
+
REQ["Requête avec un en-tête Origin"] --> WS{"réponse HTTP ?"}
|
|
45
|
+
WS -->|non = WebSocket| SKIP["no-op — le WS a sa propre garde<br/>(checkWebsocketOrigin, anti-CSWSH)"]
|
|
46
|
+
WS -->|oui| PF{"preflight ?<br/>OPTIONS + Access-Control-Request-Method"}
|
|
47
|
+
PF -->|oui| PH["Cors.preflightHeaders(origin)"]
|
|
48
|
+
PF -->|non = requête réelle| AH["Cors.actualHeaders(origin)"]
|
|
49
|
+
PH --> WL{"origine dans l'allowlist ?"}
|
|
50
|
+
AH --> WL
|
|
51
|
+
WL -->|non| NONE["null → AUCUN en-tête posé<br/>le navigateur bloque, 0 info divulguée"]
|
|
52
|
+
WL -->|oui| SET["Access-Control-Allow-*<br/>+ Vary: Origin si l'origine est reflétée"]
|
|
53
|
+
PH --> C204["204 — court-circuit total :<br/>ni routing, ni parse, ni authentification"]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Deux moments distincts, une seule allowlist. Le **preflight** est l'`OPTIONS` que le navigateur envoie
|
|
57
|
+
_de lui-même_, avant une requête « non simple », pour demander la permission. La **requête réelle** est
|
|
58
|
+
celle que ton code a écrite. Les deux consultent la même liste d'origines : une origine absente ⇒
|
|
59
|
+
**aucun en-tête** ⇒ le navigateur bloque tout seul.
|
|
60
|
+
|
|
61
|
+
## 📖 Lexique
|
|
62
|
+
|
|
63
|
+
| Terme | Sens |
|
|
64
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
65
|
+
| **Origine** | Le triplet `scheme://host:port` (`https://app.example.com`). Deux ports différents = deux origines. |
|
|
66
|
+
| **SOP** | _Same-Origin Policy_ : le navigateur isole les origines et refuse par défaut la lecture cross-origin. |
|
|
67
|
+
| **CORS** | _Cross-Origin Resource Sharing_ : le protocole par lequel le serveur déclare ses exceptions à la SOP. |
|
|
68
|
+
| **Preflight** | Requête `OPTIONS` d'autorisation, envoyée par le navigateur **avant** une requête non simple. |
|
|
69
|
+
| **Requête simple** | GET/HEAD/POST avec un `Content-Type` basique — pas de preflight, le navigateur envoie directement. |
|
|
70
|
+
| **Credentials** | Cookies / `Authorization` transportés cross-origin. Exige un opt-in explicite des deux côtés. |
|
|
71
|
+
| **Reflet d'origine** | Renvoyer l'origine du client comme valeur `Access-Control-Allow-Origin`, au lieu du joker `*`. |
|
|
72
|
+
| `Vary: Origin` | Dit aux caches que la réponse **dépend** de l'`Origin` — sans lui, un cache partagé mélange les origines. |
|
|
73
|
+
| **Allowlist** | La liste blanche d'origines autorisées (`cors.origins`). Match **exact**, jamais par sous-chaîne. |
|
|
74
|
+
| **CSWSH** | _Cross-Site WebSocket Hijacking_ : une page tierce ouvre un WebSocket authentifié par le cookie du visiteur. |
|
|
75
|
+
| **Fetch Standard** | La norme WHATWG qui définit le protocole CORS (elle a remplacé la recommandation W3C CORS). |
|
|
76
|
+
|
|
77
|
+
## Qu'est-ce que le CORS — et ce qu'il n'est PAS
|
|
78
|
+
|
|
79
|
+
Une analogie : la SOP est un **portier** qui, par défaut, refuse de remettre le courrier d'un immeuble
|
|
80
|
+
à quelqu'un d'un autre immeuble. CORS n'est pas un vigile de plus — c'est la **liste des voisins**
|
|
81
|
+
que le propriétaire affiche au portier : « à ceux-là, tu peux remettre le courrier ».
|
|
82
|
+
|
|
83
|
+
Trois conséquences que beaucoup de développeurs découvrent trop tard :
|
|
84
|
+
|
|
85
|
+
1. **CORS ne protège pas ton serveur.** Il s'applique dans le **navigateur**. Un `curl`, un script
|
|
86
|
+
Python, un agent — tout ce qui n'est pas un navigateur — ignore CORS et reçoit ta réponse en entier.
|
|
87
|
+
La protection du serveur, c'est le [firewall](./firewall.md) et l'authentification.
|
|
88
|
+
2. **La faille n'est donc jamais « CORS absent », mais « CORS trop permissif ».** Refléter
|
|
89
|
+
_n'importe quelle_ origine **avec credentials**, c'est autoriser tout site du web à lire les données
|
|
90
|
+
authentifiées de tes utilisateurs, avec leur propre cookie de session. C'est le vecteur n°1 des
|
|
91
|
+
fuites CORS recensées par l'OWASP.
|
|
92
|
+
3. **CORS ≠ CSRF.** CORS régit la **lecture** cross-origin d'une réponse ; le [CSRF](./csrf.md) protège
|
|
93
|
+
l'**écriture** (une mutation déclenchée à l'insu du visiteur). Les deux se croisent : une origine
|
|
94
|
+
que tu autorises explicitement en CORS n'est pas traitée comme une tentative CSRF (voir plus bas).
|
|
95
|
+
|
|
96
|
+
> [!IMPORTANT]
|
|
97
|
+
> Ouvrir une origine en CORS, c'est autoriser **le JavaScript de cette origine à lire tes réponses**,
|
|
98
|
+
> pas juste « à t'appeler ». Si la route renvoie des données d'un utilisateur connecté et que
|
|
99
|
+
> `credentials` est actif, l'origine ajoutée devient de facto un lecteur légitime de ces données.
|
|
100
|
+
|
|
101
|
+
## La vision Nodefony — une politique pure, fail-safe, verrouillée au boot
|
|
102
|
+
|
|
103
|
+
`Cors` (`cors.ts:33`) est une classe **pure et synchrone** : elle ne touche ni au réseau, ni à la
|
|
104
|
+
requête — elle prend une origine et renvoie la table des en-têtes à poser, ou `null`. Elle est
|
|
105
|
+
instanciée **une seule fois au boot** par le firewall, si et seulement si la section est activée
|
|
106
|
+
(`firewall.ts:213`). Conséquence directe : la politique est **testable sans serveur**, et son coût par
|
|
107
|
+
requête se réduit à une lecture de `Set` (voir Performance).
|
|
108
|
+
|
|
109
|
+
Trois invariants de sécurité, tenus par construction :
|
|
110
|
+
|
|
111
|
+
- **Jamais `*` avec credentials.** La combinaison est **rejetée au boot** par un `refine` Zod
|
|
112
|
+
(`config.ts:144`) — le navigateur la refuserait de toute façon. Défense en profondeur : même
|
|
113
|
+
instanciée à la main avec cette combinaison, `Cors.#allowOrigin()` **reflète l'origine** au lieu
|
|
114
|
+
d'émettre `*` (`cors.ts:58`).
|
|
115
|
+
- **Reflet d'origine ⇒ `Vary: Origin`.** Dès que la valeur `Allow-Origin` n'est pas `*`, la politique
|
|
116
|
+
ajoute `Vary` elle-même, sans que l'appelant ait à y penser (`Cors.reflectsOrigin()`, `cors.ts:63` ;
|
|
117
|
+
posé en `cors.ts:81` et `cors.ts:94`). Sans lui, un cache partagé servirait à une origine la réponse
|
|
118
|
+
taillée pour une autre — un empoisonnement de cache.
|
|
119
|
+
- **Origine inconnue ⇒ `null` ⇒ aucun en-tête.** Pas de message d'erreur, pas de 403 bavard
|
|
120
|
+
(`cors.ts:74`, `cors.ts:92`). La réponse part normalement mais n'est pas partageable : le navigateur
|
|
121
|
+
bloque, et un attaquant n'apprend **rien** sur le contenu de ton allowlist.
|
|
122
|
+
|
|
123
|
+
Le contrat d'entrée est `ICorsOptions` (`cors.ts:4`) — exactement le sous-ensemble `cors` de la config
|
|
124
|
+
du module, rien de plus. La sortie est une table nom → valeur (`CorsHeaders`, `cors.ts:15`) que
|
|
125
|
+
l'appelant recopie sur la réponse.
|
|
126
|
+
|
|
127
|
+
## 🚀 Démarrage rapide
|
|
128
|
+
|
|
129
|
+
**Le besoin.** Ton API Nodefony sert `https://api.example.com`. Ton front est déployé sur
|
|
130
|
+
`https://app.example.com` — **une autre origine**. Il s'authentifie avec le cookie de session (BFF), et
|
|
131
|
+
il affiche une pagination qui lit un en-tête `X-Total-Count`. Sans configuration, le navigateur bloque
|
|
132
|
+
chaque appel du front.
|
|
133
|
+
|
|
134
|
+
### 1. La config — dans `nodefony.config.ts`
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// nodefony.config.ts (extrait généré par `nodefony create app`, puis complété)
|
|
138
|
+
use("@nodefony/security", {
|
|
139
|
+
cors: {
|
|
140
|
+
// Allowlist EXACTE : scheme + host + port. Un sous-domaine n'est PAS inclus.
|
|
141
|
+
origins: ["https://app.example.com"],
|
|
142
|
+
// Le front envoie le cookie de session → opt-in obligatoire des deux côtés.
|
|
143
|
+
// Avec credentials, `*` est refusé au boot : on liste les origines.
|
|
144
|
+
credentials: true,
|
|
145
|
+
// Ce que le JS du front a le droit de LIRE dans la réponse (au-delà des
|
|
146
|
+
// en-têtes « sûrs » que le navigateur expose toujours).
|
|
147
|
+
exposedHeaders: ["X-Total-Count"],
|
|
148
|
+
// Le navigateur met le résultat du preflight en cache 10 minutes.
|
|
149
|
+
maxAgeS: 600,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 2. Le controller — rien de spécifique à CORS
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// nodefony/controllers/ArticleController.ts — complet, compile tel quel
|
|
158
|
+
import { controller, Controller, Get } from "@nodefony/framework";
|
|
159
|
+
|
|
160
|
+
@controller("/api/articles")
|
|
161
|
+
class ArticleController extends Controller {
|
|
162
|
+
// Aucun code CORS ici : la politique est GLOBALE et posée AVANT le routing.
|
|
163
|
+
// Un preflight n'atteint même jamais cette classe.
|
|
164
|
+
@Get("/")
|
|
165
|
+
async list() {
|
|
166
|
+
return this.renderJson([{ id: 1, title: "Hello" }]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default ArticleController;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 3. Ce qu'on observe
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# 1) LE PREFLIGHT — celui que le navigateur envoie tout seul avant un fetch non simple.
|
|
177
|
+
curl -si -X OPTIONS http://localhost:5151/api/articles \
|
|
178
|
+
-H 'Origin: https://app.example.com' \
|
|
179
|
+
-H 'Access-Control-Request-Method: GET'
|
|
180
|
+
# HTTP/1.1 204 No Content
|
|
181
|
+
# Access-Control-Allow-Origin: https://app.example.com
|
|
182
|
+
# Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
|
183
|
+
# Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With
|
|
184
|
+
# Access-Control-Max-Age: 600
|
|
185
|
+
# Access-Control-Allow-Credentials: true
|
|
186
|
+
# Vary: Origin
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# 2) LA REQUÊTE RÉELLE — Expose-Headers apparaît ici, jamais au preflight.
|
|
191
|
+
curl -si http://localhost:5151/api/articles -H 'Origin: https://app.example.com'
|
|
192
|
+
# HTTP/1.1 200 OK
|
|
193
|
+
# Access-Control-Allow-Origin: https://app.example.com
|
|
194
|
+
# Access-Control-Allow-Credentials: true
|
|
195
|
+
# Access-Control-Expose-Headers: X-Total-Count
|
|
196
|
+
# Vary: Origin
|
|
197
|
+
|
|
198
|
+
# 3) UNE ORIGINE INCONNUE — la réponse part, SANS aucun en-tête CORS.
|
|
199
|
+
curl -si http://localhost:5151/api/articles -H 'Origin: https://evil.com'
|
|
200
|
+
# HTTP/1.1 200 OK
|
|
201
|
+
# (aucun Access-Control-* → le navigateur refuse de livrer le corps au JS ;
|
|
202
|
+
# curl, lui, voit tout : CORS s'applique dans le navigateur, pas au serveur)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## ⚙️ Configuration et mises en situation
|
|
206
|
+
|
|
207
|
+
La section `cors` de la config du module (`corsSchema`, `config.ts:117` ; branchée à la racine en
|
|
208
|
+
`config.ts:117`). Toutes les clés ont un défaut sûr — une section omise donne une politique **fermée**.
|
|
209
|
+
|
|
210
|
+
<!-- prettier-ignore -->
|
|
211
|
+
| Option | Type | Défaut | Effet |
|
|
212
|
+
| --- | --- | --- | --- |
|
|
213
|
+
| `enabled` | `boolean` | `true` | `false` ⇒ aucune politique instanciée, `handleCors` est un no-op total. |
|
|
214
|
+
| `origins` | `string[]` | `[]` | L'allowlist. **`[]` = aucune origine autorisée** (fermé par défaut). |
|
|
215
|
+
| `credentials` | `boolean` | `false` | Autorise cookies/`Authorization` cross-origin. Interdit avec `origins:["*"]`. |
|
|
216
|
+
| `methods` | `string[]` | `GET, POST, PUT, PATCH, DELETE, OPTIONS` | Annoncées au preflight via `Access-Control-Allow-Methods`. |
|
|
217
|
+
| `allowedHeaders` | `string[]` | `Authorization, Content-Type, X-Requested-With` | En-têtes de requête que le front a le droit d'envoyer. |
|
|
218
|
+
| `exposedHeaders` | `string[]` | `[]` | En-têtes de **réponse** que le JS a le droit de lire. Requête réelle seulement. |
|
|
219
|
+
| `maxAgeS` | `int` | `600` | Durée de cache du preflight, en secondes. |
|
|
220
|
+
|
|
221
|
+
> [!TIP]
|
|
222
|
+
> Le défaut `origins: []` avec `enabled: true` n'est pas une incohérence : la politique existe, mais
|
|
223
|
+
> refuse **toutes** les origines. Une app qui ne configure rien n'a donc aucune ouverture cross-origin
|
|
224
|
+
> accidentelle — il faut un geste explicite pour ouvrir.
|
|
225
|
+
|
|
226
|
+
### Situation 1 — un front sur un autre domaine, avec session (le cas courant)
|
|
227
|
+
|
|
228
|
+
Ton SPA est sur `https://app.example.com`, ton API sur `https://api.example.com`, et l'utilisateur est
|
|
229
|
+
connecté par cookie. Il faut **deux** opt-ins : le serveur (`credentials: true`) et le client
|
|
230
|
+
(`fetch(url, { credentials: "include" })`).
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
cors: {
|
|
234
|
+
origins: ["https://app.example.com"],
|
|
235
|
+
credentials: true,
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
| Le client envoie… | Le serveur répond… |
|
|
240
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
241
|
+
| `Origin: https://app.example.com` | `Allow-Origin: https://app.example.com` + `Allow-Credentials: true` + `Vary: Origin` |
|
|
242
|
+
| `Origin: https://app.example.com:8443` | rien — le **port** fait partie de l'origine, match exact |
|
|
243
|
+
| `Origin: https://sub.app.example.com` | rien — un sous-domaine n'est **pas** couvert par le parent |
|
|
244
|
+
| `Origin: http://app.example.com` | rien — le **scheme** fait partie de l'origine (downgrade refusé) |
|
|
245
|
+
| aucun `Origin` (même origine, ou `curl`) | rien — la requête suit le pipeline normal (`firewall.ts:997`) |
|
|
246
|
+
|
|
247
|
+
### Situation 2 — une API publique en lecture seule (le joker `*`)
|
|
248
|
+
|
|
249
|
+
Une API de documentation, de statut, de tarifs : pas d'identité, pas de cookie, n'importe qui peut la
|
|
250
|
+
lire depuis n'importe quelle page.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
cors: {
|
|
254
|
+
origins: ["*"],
|
|
255
|
+
credentials: false, // OBLIGATOIRE avec le joker
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Ici, et seulement ici, la politique émet littéralement `*` (`cors.ts:58`) — et donc **pas de
|
|
260
|
+
`Vary: Origin`** : la réponse est identique pour tout le monde, elle est cachable telle quelle.
|
|
261
|
+
|
|
262
|
+
### Situation 3 — le contre-exemple piégeux : `*` + credentials
|
|
263
|
+
|
|
264
|
+
C'est la configuration qu'on écrit « pour débloquer le dev » et qui devient une fuite en production.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// ❌ REFUSÉ AU BOOT — l'app ne démarre pas
|
|
268
|
+
cors: { origins: ["*"], credentials: true }
|
|
269
|
+
|
|
270
|
+
// ✅ Lister les origines explicitement
|
|
271
|
+
cors: { origins: ["https://app.example.com", "https://admin.example.com"], credentials: true }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
> [!WARNING]
|
|
275
|
+
> `origins: ["*"]` **avec** `credentials: true` est rejeté par le schéma Zod au démarrage
|
|
276
|
+
> (`config.ts:144`), avec un message qui dit quoi faire. La raison n'est pas cosmétique : cette
|
|
277
|
+
> combinaison, si un serveur la contournait en reflétant chaque origine, laisserait **tout site du
|
|
278
|
+
> web** lire les réponses authentifiées de tes utilisateurs. Ne « corrige » jamais cette erreur en
|
|
279
|
+
> reflétant l'origine reçue sans la valider.
|
|
280
|
+
|
|
281
|
+
### Situation 4 — le front doit lire un en-tête que tu ajoutes
|
|
282
|
+
|
|
283
|
+
Ton API pagine et renvoie `X-Total-Count`. Le JS appelle `response.headers.get("X-Total-Count")` et
|
|
284
|
+
récupère… `null`. Ce n'est pas un bug : le navigateur n'expose au JS qu'une poignée d'en-têtes sûrs.
|
|
285
|
+
Tout le reste doit être **déclaré**.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
cors: {
|
|
289
|
+
origins: ["https://app.example.com"],
|
|
290
|
+
exposedHeaders: ["X-Total-Count", "X-Request-Id", "ETag"],
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Cet en-tête n'est posé **que sur la requête réelle** (`cors.ts:96`), jamais sur le preflight — un
|
|
295
|
+
preflight ne transporte pas de corps, il n'y a rien à exposer. Symétrie à ne pas confondre :
|
|
296
|
+
`allowedHeaders` = ce que le front a le droit d'**envoyer** ; `exposedHeaders` = ce qu'il a le droit de
|
|
297
|
+
**lire**.
|
|
298
|
+
|
|
299
|
+
## 🏗️ Architecture interne — où la politique s'insère
|
|
300
|
+
|
|
301
|
+
```mermaid
|
|
302
|
+
sequenceDiagram
|
|
303
|
+
participant B as Navigateur
|
|
304
|
+
participant K as HttpKernel.handleHttp
|
|
305
|
+
participant F as Firewall.handleCors
|
|
306
|
+
participant C as Cors (politique pure)
|
|
307
|
+
participant R as Router / Controller
|
|
308
|
+
|
|
309
|
+
B->>K: OPTIONS /api/articles (Origin + Access-Control-Request-Method)
|
|
310
|
+
K->>F: handleCors(context)
|
|
311
|
+
F->>C: preflightHeaders(origin)
|
|
312
|
+
C-->>F: table d'en-têtes (ou null)
|
|
313
|
+
F-->>K: 204
|
|
314
|
+
K-->>B: 204 + Access-Control-* (le Router n'a jamais été appelé)
|
|
315
|
+
|
|
316
|
+
B->>K: GET /api/articles (Origin)
|
|
317
|
+
K->>F: handleCors(context)
|
|
318
|
+
F->>C: actualHeaders(origin)
|
|
319
|
+
C-->>F: table d'en-têtes (ou null)
|
|
320
|
+
F-->>K: undefined
|
|
321
|
+
K->>R: routing, firewall, controller…
|
|
322
|
+
R-->>B: 200 + Access-Control-*
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`Firewall.handleCors()` (`firewall.ts:991`) est appelé **en tête de** `HttpKernel.handleHttp()`
|
|
326
|
+
(`http-kernel.ts:1258`), à la ligne `http-kernel.ts:1258` — **avant le routing**. La raison est
|
|
327
|
+
concrète : un preflight `OPTIONS /api/articles` n'a **pas de route déclarée** ; s'il traversait le
|
|
328
|
+
router, il repartirait en 405. Et selon le Fetch Standard, un preflight ne transporte jamais de
|
|
329
|
+
credentials — il ne doit donc ni s'authentifier, ni exécuter le moindre code applicatif.
|
|
330
|
+
|
|
331
|
+
Quatre sorties en no-op, dans cet ordre (`firewall.ts:797`) :
|
|
332
|
+
|
|
333
|
+
1. CORS désactivé ⇒ `#cors` est `null`, retour immédiat ;
|
|
334
|
+
2. pas d'en-tête `Origin` ⇒ requête same-origin ou client non-navigateur (`firewall.ts:587`) ;
|
|
335
|
+
3. la réponse n'expose pas `setHeader` ⇒ c'est un **WebSocket**, il n'y a pas d'en-tête HTTP à poser
|
|
336
|
+
(`firewall.ts:1013`) ;
|
|
337
|
+
4. origine hors allowlist ⇒ la table est `null`, aucun en-tête n'est posé — mais un preflight reste
|
|
338
|
+
court-circuité en 204 (`firewall.ts:822`).
|
|
339
|
+
|
|
340
|
+
**La détection du preflight est stricte** : méthode `OPTIONS` **et** présence de
|
|
341
|
+
`Access-Control-Request-Method` (`firewall.ts:808`). Un `OPTIONS` nu — celui d'un client qui interroge
|
|
342
|
+
les méthodes supportées d'une route — est donc traité comme une requête réelle et continue le pipeline.
|
|
343
|
+
|
|
344
|
+
### Ce que chaque moment pose
|
|
345
|
+
|
|
346
|
+
| Moment | Déclencheur | En-têtes posés | Ancrage |
|
|
347
|
+
| ------------------ | ------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------ |
|
|
348
|
+
| **Preflight** | `OPTIONS` + `Access-Control-Request-Method` | `Allow-Origin`, `Allow-Methods`, `Allow-Headers`, `Max-Age` (+ `Vary`, + `Allow-Credentials`) | `cors.ts:72` |
|
|
349
|
+
| **Requête réelle** | tout le reste, avec un `Origin` | `Allow-Origin` (+ `Vary`, + `Allow-Credentials`, + **`Expose-Headers`**) | `cors.ts:90` |
|
|
350
|
+
|
|
351
|
+
Deux nuances utiles :
|
|
352
|
+
|
|
353
|
+
- **`Allow-Headers` est statique**, dérivé de la config — la politique ne réfléchit pas le
|
|
354
|
+
`Access-Control-Request-Headers` du client (`cors.ts:78`). Ce que tu déclares est ce qui est annoncé,
|
|
355
|
+
point. Un en-tête custom non déclaré fait échouer le preflight côté navigateur.
|
|
356
|
+
- **Les fichiers statiques sont couverts.** `handleCors` s'exécute avant le fallback `serve-static`
|
|
357
|
+
(`http-kernel.ts:1312`) : une police ou une image servie cross-origin reçoit les mêmes en-têtes que
|
|
358
|
+
tes routes.
|
|
359
|
+
|
|
360
|
+
Le contrat est publié dans l'interface du firewall (`IFirewall.ts:34`) : `number | undefined` — `204`
|
|
361
|
+
signifie « je suis un preflight, réponds et arrête-toi ».
|
|
362
|
+
|
|
363
|
+
## 🛡️ CORS, CSRF et en-têtes de sécurité — qui protège quoi
|
|
364
|
+
|
|
365
|
+
Trois briques voisines, souvent confondues. Une seule ligne chacune :
|
|
366
|
+
|
|
367
|
+
| Brique | Régit… | S'applique… | Menace bloquée |
|
|
368
|
+
| ---------------------------------------- | -------------------------------------- | ---------------------- | ------------------------------------ |
|
|
369
|
+
| **CORS** | la **lecture** cross-origin | dans le navigateur | fuite de données authentifiées |
|
|
370
|
+
| **[CSRF](./csrf.md)** | l'**écriture** cross-site | au serveur (rejet 403) | mutation déclenchée à l'insu du user |
|
|
371
|
+
| **[En-têtes](./headers.md)** (CSP, COOP) | ce que la **page** a le droit de faire | dans le navigateur | XSS, injection, fenêtres croisées |
|
|
372
|
+
|
|
373
|
+
Les deux premières se parlent. Au boot, la liste des origines de confiance CSRF est l'**union** de
|
|
374
|
+
`csrf.trustedOrigins` et de `cors.origins` (`firewall.ts:589`) : ce que tu autorises explicitement en
|
|
375
|
+
CORS ne peut pas être, au même instant, traité comme une tentative CSRF.
|
|
376
|
+
|
|
377
|
+
L'inverse n'est pas vrai, et c'est délibéré : `csrf.trustedOrigins` déclare un **alias de domaine**
|
|
378
|
+
légitime de ton app (une façade multi-domaine) sans pour autant exposer tes réponses au JS de cette
|
|
379
|
+
origine (`config.ts:180`). Ajouter une origine à `cors.origins` est **plus** permissif que l'ajouter à
|
|
380
|
+
`csrf.trustedOrigins`.
|
|
381
|
+
|
|
382
|
+
## 🔌 Et le WebSocket ?
|
|
383
|
+
|
|
384
|
+
**Les navigateurs n'appliquent pas CORS aux WebSockets.** Une page tierce peut ouvrir un
|
|
385
|
+
`new WebSocket("wss://api.example.com/…")` et le handshake partira **avec le cookie de session de la
|
|
386
|
+
victime** : c'est le CSWSH. C'est pourquoi `handleCors` s'arrête net sur un contexte WS
|
|
387
|
+
(`firewall.ts:991`) — il n'y aurait rien à protéger avec des en-têtes que personne ne lit.
|
|
388
|
+
|
|
389
|
+
La garde équivalente vit dans le transport : `HttpKernel.checkWebsocketOrigin()`
|
|
390
|
+
(`http-kernel.ts:599`) valide l'`Origin` **au handshake**, avant l'accept, et ferme en code WS `1008`
|
|
391
|
+
si elle est refusée. Sa doctrine :
|
|
392
|
+
|
|
393
|
+
- **same-origin par défaut** : l'`Origin` du handshake doit correspondre au `Host` servi ;
|
|
394
|
+
- **loopback toléré en development** uniquement, pour le cross-port Vite ↔ serveur ;
|
|
395
|
+
- **allowlist explicite** `allowedOrigins` par type de serveur pour une SPA cross-origin en production
|
|
396
|
+
(compilée une seule fois puis mémoïsée) ;
|
|
397
|
+
- **pas d'`Origin` ⇒ accepté** : un client non-navigateur n'a aucun besoin de CSWSH, il se connecte
|
|
398
|
+
directement — refuser ne protégerait personne et casserait tous les clients légitimes.
|
|
399
|
+
|
|
400
|
+
Retenir : `cors.origins` ouvre le **HTTP**, `allowedOrigins` (config `@nodefony/http`) ouvre le **WS**.
|
|
401
|
+
Deux réglages distincts, parce que deux mécanismes navigateur distincts.
|
|
402
|
+
|
|
403
|
+
## 📜 Normes appliquées
|
|
404
|
+
|
|
405
|
+
| Sujet | Norme | Comment le code s'y conforme |
|
|
406
|
+
| --------------------------------- | --------------------------- | --------------------------------------------------------------------------- |
|
|
407
|
+
| Protocole CORS | Fetch Standard (WHATWG) | `Cors` (`cors.ts:33`) — preflight vs requête réelle séparés |
|
|
408
|
+
| Preflight sans credentials | Fetch Standard | court-circuit en 204 avant auth/routing (`firewall.ts:822`) |
|
|
409
|
+
| `*` incompatible avec credentials | Fetch Standard · OWASP CORS | rejet au boot (`config.ts:144`) + reflet défensif (`cors.ts:58`) |
|
|
410
|
+
| Correction de cache | RFC 9110 (`Vary`) | `Vary: Origin` dès que l'origine est reflétée (`cors.ts:81`, `cors.ts:94`) |
|
|
411
|
+
| Comparaison d'origines | RFC 6454 (Web Origin) | match **exact** `scheme://host:port` — `Cors.#allowOrigin()` (`cors.ts:57`) |
|
|
412
|
+
| Anti-CSWSH | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:599`) |
|
|
413
|
+
|
|
414
|
+
## ⚡ Performance & mémoire
|
|
415
|
+
|
|
416
|
+
La politique est **précalculée au boot** : les listes `methods`, `allowedHeaders`, `exposedHeaders` et
|
|
417
|
+
`maxAgeS` sont jointes/converties une fois dans le constructeur (`cors.ts:42`), jamais par requête. Il
|
|
418
|
+
ne reste à l'exécution qu'un `Set.has()` sur l'origine.
|
|
419
|
+
|
|
420
|
+
Le coût par requête est donc :
|
|
421
|
+
|
|
422
|
+
- **0 pour une requête same-origin** — pas d'en-tête `Origin`, sortie immédiate (`firewall.ts:997`) ;
|
|
423
|
+
- **0 pour un WebSocket** — sortie sur l'absence de `setHeader` (`firewall.ts:1013`) ;
|
|
424
|
+
- **0 si la section est désactivée** — `#cors` reste `null`, aucun objet n'est alloué (`firewall.ts:166`) ;
|
|
425
|
+
- **une petite table d'en-têtes** allouée uniquement pour une requête cross-origin autorisée. Une
|
|
426
|
+
origine refusée n'alloue rien du tout (retour `null` avant construction de la table, `cors.ts:74`).
|
|
427
|
+
|
|
428
|
+
## 📡 Observabilité — Studio
|
|
429
|
+
|
|
430
|
+
La configuration CORS **résolue** (celle qui tourne réellement, pas le fichier source) est exposée par
|
|
431
|
+
`Firewall.describe()` (`firewall.ts:505`), qui délègue à `Firewall.#describeDefenses()`
|
|
432
|
+
(`firewall.ts:575`). La projection CORS y expose `origins`, `credentials`, `methods`,
|
|
433
|
+
`allowedHeaders`, `exposedHeaders` et `maxAgeS` (`firewall.ts:594`) — aucun secret ne transite par
|
|
434
|
+
cette surface.
|
|
435
|
+
|
|
436
|
+
- **Data plane** : `GET /nodefony/security/api/firewall` (`SecurityAdminApi.ts:348`), protégé
|
|
437
|
+
`ROLE_NODEFONY_ADMIN`.
|
|
438
|
+
- **Écran** : console **Firewall** → section _Défenses_ (`FirewallDefenses`,
|
|
439
|
+
`FirewallDefenses.tsx:114`), carte CORS à côté des cartes CSRF, en-têtes et throttle.
|
|
440
|
+
- **Schéma de config** : `securityConfigJsonSchema()` alimente le formulaire d'édition de Studio —
|
|
441
|
+
la section `cors` y apparaît avec ses libellés et défauts, sans UI écrite à la main.
|
|
442
|
+
|
|
443
|
+
Utile en incident : comparer ce que Studio affiche avec ce que tu crois avoir déployé règle en dix
|
|
444
|
+
secondes les « pourtant j'ai bien mis l'origine ».
|
|
445
|
+
|
|
446
|
+
## ⚠️ Pièges (symptôme → cause → correction)
|
|
447
|
+
|
|
448
|
+
| Symptôme | Cause | Correction |
|
|
449
|
+
| --------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
450
|
+
| Le boot échoue sur la config CORS | `origins:["*"]` **et** `credentials:true` (`config.ts:144`) | Lister les origines, ou passer `credentials:false` |
|
|
451
|
+
| Requête bloquée alors que l'origine « est » dans la liste | Match **exact** : port, scheme ou sous-domaine divergent | Écrire l'origine complète `scheme://host:port`, une entrée par variante |
|
|
452
|
+
| `curl` fonctionne, le navigateur non | CORS s'applique dans le navigateur, pas au serveur | Normal — reproduire avec un `Origin` explicite (`curl -H 'Origin: …'`) |
|
|
453
|
+
| Cookie non envoyé malgré `credentials:true` | Opt-in serveur seul : le client n'a pas `credentials: "include"` | Activer les deux côtés (et un cookie `SameSite=None; Secure` en cross-site) |
|
|
454
|
+
| `response.headers.get("X-…")` renvoie `null` | En-tête non déclaré dans `exposedHeaders` (`cors.ts:96`) | L'ajouter à `cors.exposedHeaders` |
|
|
455
|
+
| Le preflight échoue sur un en-tête custom | `allowedHeaders` est statique, il ne reflète pas la demande du client (`cors.ts:78`) | Déclarer l'en-tête dans `cors.allowedHeaders` |
|
|
456
|
+
| Un cache sert la réponse d'une origine à une autre | `Vary: Origin` écrasé en aval (la politique le pose, `cors.ts:81`) | Ne pas `setHeader("Vary", …)` dans un controller — utiliser `appendHeader` |
|
|
457
|
+
| `OPTIONS` renvoie 405 au lieu de 204 | Requête `OPTIONS` **sans** `Access-Control-Request-Method` : ce n'est pas un preflight | Envoyer l'en-tête, ou déclarer une route `OPTIONS` |
|
|
458
|
+
| Page tierce qui ouvre un WebSocket authentifié | CORS ne couvre pas le WS | C'est `checkWebsocketOrigin` qui garde (`http-kernel.ts:599`) — vérifier `allowedOrigins` |
|
|
459
|
+
| Ouverture CORS « temporaire » restée en production | `origins:["*"]` posé en dev | Vérifier la valeur **résolue** dans Studio, pas le fichier source |
|
|
460
|
+
|
|
461
|
+
## 🧪 Tests & couverture
|
|
462
|
+
|
|
463
|
+
Trois familles couvrent la brique — les compteurs exacts vivent dans la carte de l'aperçu, régénérée
|
|
464
|
+
depuis vitest, jamais figés ici :
|
|
465
|
+
|
|
466
|
+
- **unitaires** — `cors.test.ts` (`src/packages/@nodefony/security/tests/unit/`) : la matrice
|
|
467
|
+
fonctionnelle de la politique pure. Allowlist et reflet, joker sans credentials, credentials
|
|
468
|
+
(reflet obligatoire, jamais `*`), `exposedHeaders` présent sur la requête réelle et **absent** du
|
|
469
|
+
preflight, `reflectsOrigin`.
|
|
470
|
+
- **attaque (red-team)** — `cors.attack.test.ts` (même dossier), dérivé de la **menace** et non de
|
|
471
|
+
l'implémentation. Vecteur central : le _allowlist bypass_ — neuf origines qu'un comparateur naïf
|
|
472
|
+
(sous-chaîne, préfixe, `endsWith`, casse, slash final, `userinfo`, port ajouté, downgrade de scheme)
|
|
473
|
+
accepterait à tort, plus le cas `Origin: null` des iframes sandbox et redirections. Chacune doit
|
|
474
|
+
renvoyer `null` au preflight **et** à la requête réelle. Un contrôle **positif** garde le banc
|
|
475
|
+
honnête : sans lui, « tout refuser » serait trivialement vert.
|
|
476
|
+
- **intégration (serveur réel)** — `cors.test.ts` (`src/packages/@nodefony/http/nodefony/tests/http/`) :
|
|
477
|
+
le câblage de bout en bout sur le serveur live, origine de confiance `https://trusted.example`.
|
|
478
|
+
Preflight autorisé → 204 + en-têtes + `Vary` ; preflight non autorisé → 204 **sans** `Allow-Origin` ;
|
|
479
|
+
requête réelle reflétée ; requête same-origin sans aucun en-tête CORS.
|
|
480
|
+
|
|
481
|
+
Ce qui **n'existe pas** et n'est pas nécessaire : pas de test de charge dédié à CORS (le chemin chaud
|
|
482
|
+
est un `Set.has()` couvert par les bancs de charge HTTP généraux), pas de banc de contrat multi-backend
|
|
483
|
+
(la brique ne persiste rien, elle n'a pas d'adapter).
|
|
484
|
+
|
|
485
|
+
Couverture : `npm run coverage` dans `@nodefony/security`. Revue de sécurité de la brique → skill
|
|
486
|
+
`nodefony-security-review` (mode red/blue-team), conformité aux normes → skill `nodefony-rfc`.
|
|
487
|
+
|
|
488
|
+
## 🔗 Pour aller plus loin
|
|
489
|
+
|
|
490
|
+
- ⬆️ **Retour au hub** : [Sécurité — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
491
|
+
- 🧭 **Pages sœurs** : [En-têtes de sécurité](headers.md) · [CSRF](csrf.md)
|
|
492
|
+
|
|
493
|
+
- Protéger les **mutations** cross-site (distinct de CORS) → [csrf](./csrf.md)
|
|
494
|
+
- Le pare-feu qui pose les en-têtes et court-circuite le preflight → [firewall](./firewall.md)
|
|
495
|
+
- CSP, HSTS, COOP/COEP/CORP → [headers](./headers.md)
|
|
496
|
+
- Vue d'ensemble du module → [index](./index.md)
|
|
497
|
+
- Où CORS s'insère dans le pipeline → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)
|