@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/headers.md
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "En-têtes de sécurité — le contrat passé au navigateur"
|
|
3
|
+
navTitle: En-têtes de sécurité
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/security"
|
|
6
|
+
topic: headers
|
|
7
|
+
coverageModule: security
|
|
8
|
+
coverageFiles: "securityHeaders.ts,csp.ts"
|
|
9
|
+
section: "Sécurité"
|
|
10
|
+
audience: [developer, devops]
|
|
11
|
+
tags:
|
|
12
|
+
[
|
|
13
|
+
security,
|
|
14
|
+
headers,
|
|
15
|
+
csp,
|
|
16
|
+
nonce,
|
|
17
|
+
hsts,
|
|
18
|
+
clickjacking,
|
|
19
|
+
nosniff,
|
|
20
|
+
referrer-policy,
|
|
21
|
+
coop,
|
|
22
|
+
coep,
|
|
23
|
+
corp,
|
|
24
|
+
permissions-policy,
|
|
25
|
+
owasp,
|
|
26
|
+
]
|
|
27
|
+
version: "doc"
|
|
28
|
+
status: stable
|
|
29
|
+
updated: 2026-07-19
|
|
30
|
+
source: "src/packages/@nodefony/security/docs/headers.md"
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# En-têtes de sécurité — le contrat passé au navigateur
|
|
34
|
+
|
|
35
|
+
> Ton serveur ne contrôle pas le navigateur de tes visiteurs — il ne peut que **lui donner des
|
|
36
|
+
> ordres**, et ces ordres sont des en-têtes HTTP. Une douzaine de lignes ferment des classes
|
|
37
|
+
> entières d'attaques : XSS, clickjacking, sniffing MIME, fuite d'URL, downgrade HTTPS. Nodefony
|
|
38
|
+
> les pose en **deux couches, une seule autorité par en-tête** : le socle **transport**
|
|
39
|
+
> (`@nodefony/http`, dès l'entrée brute — couvre aussi les fichiers statiques et les erreurs) et la
|
|
40
|
+
> couche **applicative** (`@nodefony/security`, dans le pipeline — CSP, Referrer-Policy, isolation
|
|
41
|
+
> cross-origin). Ancré sur `SecurityHeaders` (`securityHeaders.ts:42`) et
|
|
42
|
+
> `Firewall.applySecurityHeaders()` (`firewall.ts:1029`).
|
|
43
|
+
|
|
44
|
+
📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **En-têtes de sécurité**
|
|
45
|
+
|
|
46
|
+
## 🧠 Le modèle mental — deux couches, deux moments
|
|
47
|
+
|
|
48
|
+
Un en-tête de sécurité n'a de valeur que s'il est **sur toutes les réponses**. Le piège classique
|
|
49
|
+
n'est pas d'en oublier un : c'est de le poser **de façon inégale** — présent sur les routes de
|
|
50
|
+
contrôleur, absent sur un fichier statique ou une page d'erreur 404, c'est-à-dire exactement là où
|
|
51
|
+
un attaquant dépose son contenu.
|
|
52
|
+
|
|
53
|
+
D'où le découpage : ce qui doit couvrir **tout ce qui sort du process** est posé au plus tôt ; ce
|
|
54
|
+
qui dépend de la **réponse applicative** (le CSP, qui doit connaître le nonce et la route) est posé
|
|
55
|
+
après le routage.
|
|
56
|
+
|
|
57
|
+
```mermaid
|
|
58
|
+
flowchart TD
|
|
59
|
+
RAW["Requête entrante"] --> T["onHttpRequest — socle TRANSPORT (@nodefony/http)<br/>X-Content-Type-Options · X-Frame-Options · HSTS (TLS seulement)"]
|
|
60
|
+
T --> COV["couvre AUSSI : fichiers statiques, 404/500,<br/>et un serveur SANS module security"]
|
|
61
|
+
T --> PIPE["pipeline : routing / resolve"]
|
|
62
|
+
PIPE --> A["applySecurityHeaders — couche APPLICATIVE (@nodefony/security)<br/>CSP · Referrer-Policy · COOP/COEP/CORP · Origin-Agent-Cluster · Permissions-Policy"]
|
|
63
|
+
A --> CSPQ{"le CSP porte-t-il<br/>un nonce ?"}
|
|
64
|
+
CSPQ -->|non| STAT["CSP figé au boot — 0 allocation par requête"]
|
|
65
|
+
CSPQ -->|oui| NONCE["cspFor(nonce) — 1 join par requête<br/>nonce généré paresseusement sur le Context"]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Une seule source par en-tête** : `@nodefony/security` ne ré-émet **jamais** les trois en-têtes
|
|
69
|
+
transport — c'est écrit noir sur blanc dans le contrat de la couche applicative
|
|
70
|
+
(`ISecurityHeadersOptions`, `securityHeaders.ts:12`). Pas de double émission, donc pas de valeurs
|
|
71
|
+
contradictoires sur la même réponse.
|
|
72
|
+
|
|
73
|
+
## 📖 Lexique
|
|
74
|
+
|
|
75
|
+
| Terme | Développé — et ce que ça veut dire |
|
|
76
|
+
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| CSP | _Content-Security-Policy_ : liste blanche des sources autorisées (scripts, styles, images…). Première barrière anti-XSS. |
|
|
78
|
+
| XSS | _Cross-Site Scripting_ : un attaquant fait exécuter **son** JavaScript dans la page de ta victime, avec ses cookies. |
|
|
79
|
+
| Nonce | _number used once_ : jeton aléatoire régénéré à **chaque requête**, qui autorise un `<script>` inline **précis** et lui seul. |
|
|
80
|
+
| Clickjacking | Ton site est chargé en `<iframe>` transparente au-dessus d'un piège : la victime croit cliquer ailleurs, elle clique chez toi. |
|
|
81
|
+
| MIME sniffing | Le navigateur ignore le `Content-Type` et **devine** le type d'un fichier — un `.txt` uploadé peut finir exécuté comme script. |
|
|
82
|
+
| HSTS | _HTTP Strict-Transport-Security_ (RFC 6797) : le navigateur mémorise « ce domaine, c'est HTTPS uniquement ». |
|
|
83
|
+
| Referrer | En-tête que le navigateur envoie au site suivant pour dire d'où l'on vient — donc une **fuite d'URL** potentielle. |
|
|
84
|
+
| COOP / COEP / CORP | _Cross-Origin **Opener** / **Embedder** / **Resource** Policy_ : trois verrous d'isolation entre origines (Spectre, vol d'assets). |
|
|
85
|
+
| OAC | _Origin-Agent-Cluster_ : demande au navigateur d'isoler l'origine dans son propre processus/heap. |
|
|
86
|
+
| Permissions-Policy | Coupe l'accès aux API sensibles du navigateur (caméra, micro, géolocalisation) pour la page **et ses iframes**. |
|
|
87
|
+
| Fragment CSP | Directives additionnelles déclarées par un module (`directive → sources`), fusionnées dans le CSP de base. |
|
|
88
|
+
| Downgrade | Un attaquant réseau force la connexion en HTTP clair pour la lire ou la modifier. |
|
|
89
|
+
|
|
90
|
+
## Qu'est-ce que c'est ? — un panneau d'instructions collé sur chaque réponse
|
|
91
|
+
|
|
92
|
+
Imagine que tu envoies un colis. Le contenu, c'est ton HTML. Les en-têtes de sécurité, c'est
|
|
93
|
+
l'**étiquette** collée dessus : « ne pas ouvrir avec un autre outil que celui-ci », « ne pas
|
|
94
|
+
transporter dans un autre camion », « interdiction de recopier l'adresse de l'expéditeur ». Le
|
|
95
|
+
transporteur — le navigateur — les respecte. Sans étiquette, il improvise, et improviser c'est
|
|
96
|
+
exactement ce qu'un attaquant attend.
|
|
97
|
+
|
|
98
|
+
Chaque en-tête ferme **une** faille concrète :
|
|
99
|
+
|
|
100
|
+
- **CSP** — un attaquant réussit à injecter `<script src="https://evil.tld/x.js">` dans un
|
|
101
|
+
commentaire de ton site ; sans CSP, le navigateur l'exécute avec la session de la victime.
|
|
102
|
+
- **X-Frame-Options / `frame-ancestors`** — un site pirate charge ta page « Supprimer mon compte »
|
|
103
|
+
en iframe invisible sous un bouton « Jouer » : la victime clique, c'est chez toi que ça s'applique.
|
|
104
|
+
- **`nosniff`** — un avatar téléversé est en réalité du JavaScript ; un navigateur « serviable »
|
|
105
|
+
devine le type et l'exécute **sur ton origine**, donc avec tes cookies.
|
|
106
|
+
- **Referrer-Policy** — un clic vers l'extérieur transmet ton URL interne complète
|
|
107
|
+
(`/admin/facture/8123?client=ACME`) dans le `Referer` du site suivant.
|
|
108
|
+
- **HSTS** — sur un Wi-Fi public, la première requête part en clair et peut être interceptée puis
|
|
109
|
+
maintenue en HTTP ; HSTS mémorisé force le HTTPS **avant** toute émission.
|
|
110
|
+
- **COOP / COEP / CORP** — isolent ton document des autres origines (fenêtres ouvrantes, ressources
|
|
111
|
+
embarquées) : réponse aux canaux auxiliaires type Spectre et au vol d'assets par inclusion.
|
|
112
|
+
|
|
113
|
+
Le détail de chaque en-tête — menace, valeur par défaut, compromis — est dans le catalogue plus bas.
|
|
114
|
+
|
|
115
|
+
## La vision Nodefony — pré-calculé au boot, quasi gratuit par requête
|
|
116
|
+
|
|
117
|
+
Un framework qui recalcule ses en-têtes à chaque requête paie ce confort en allocations. Nodefony
|
|
118
|
+
fait l'inverse : **tout ce qui est constant est calculé une fois au démarrage**.
|
|
119
|
+
|
|
120
|
+
- `SecurityHeaders` (`securityHeaders.ts:42`) construit **au boot** la table des en-têtes constants
|
|
121
|
+
(Referrer-Policy, COOP/COEP/CORP, Origin-Agent-Cluster, Permissions-Policy, et le CSP quand il est
|
|
122
|
+
statique) et la **gèle** avec `Object.freeze` (`securityHeaders.ts:77`). Par requête, le firewall
|
|
123
|
+
se contente de la parcourir et de la poser : zéro concaténation, zéro objet créé.
|
|
124
|
+
- Côté transport, même principe : `HttpKernel.computeSecurityHeaderCaches()`
|
|
125
|
+
(`http-kernel.ts:330`) précalcule la chaîne HSTS (`max-age`, `includeSubDomains`, `preload`) au
|
|
126
|
+
boot ; `onHttpRequest` (`http-kernel.ts:819`) ne fait plus que trois `setHeader`.
|
|
127
|
+
- Le seul coût variable est le **nonce CSP**, et il est **paresseux** : `Context.cspNonce`
|
|
128
|
+
(`Context.ts:253`) ne génère ses 128 bits (`randomBytes(16)` en base64) qu'à la première lecture,
|
|
129
|
+
puis mémoïse. Une réponse qui n'a aucun script inline à signer ne paie aucun appel crypto.
|
|
130
|
+
|
|
131
|
+
Le second parti pris est la **séparation d'autorité** décrite plus haut : un seul émetteur par
|
|
132
|
+
en-tête, donc un comportement prévisible et testable — le banc live vérifie les deux couches sur la
|
|
133
|
+
même réponse (`security-headers.test.ts:30`).
|
|
134
|
+
|
|
135
|
+
> [!IMPORTANT]
|
|
136
|
+
> `@nodefony/security` est **optionnel**, pas le socle. Une app Nodefony sans module security émet
|
|
137
|
+
> quand même `nosniff`, `X-Frame-Options` et HSTS : c'est du _secure-by-default_. Ce que tu perds
|
|
138
|
+
> sans security, c'est le CSP, la Referrer-Policy et l'isolation cross-origin.
|
|
139
|
+
|
|
140
|
+
## 🚀 Démarrage rapide
|
|
141
|
+
|
|
142
|
+
Point de départ : une app générée par `nodefony create app`. Les en-têtes sont **déjà actifs** — ce
|
|
143
|
+
que tu écris ci-dessous, ce sont tes **écarts** au défaut.
|
|
144
|
+
|
|
145
|
+
### 1. Déclarer la politique dans `nodefony.config.ts`
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// nodefony.config.ts — l'app n'écrit QUE ses écarts ; le reste prend le défaut du framework.
|
|
149
|
+
import { defineConfig, use } from "nodefony";
|
|
150
|
+
|
|
151
|
+
export default defineConfig(() => ({
|
|
152
|
+
modules: [
|
|
153
|
+
"@nodefony/http",
|
|
154
|
+
"@nodefony/framework",
|
|
155
|
+
use("@nodefony/security", {
|
|
156
|
+
headers: {
|
|
157
|
+
// `{{nonce}}` est substitué par un jeton FRAIS à chaque requête (cf plus bas).
|
|
158
|
+
csp:
|
|
159
|
+
"default-src 'self'; script-src 'self' 'nonce-{{nonce}}'; " +
|
|
160
|
+
"style-src 'self' 'unsafe-inline'; img-src 'self' data:; " +
|
|
161
|
+
"object-src 'none'; base-uri 'self'; form-action 'self'",
|
|
162
|
+
cspNonces: true,
|
|
163
|
+
// Ne fuiter que l'origine, et rien vers un site en clair.
|
|
164
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
165
|
+
// Isolation cross-origin : ABSENTE par défaut, on l'active explicitement.
|
|
166
|
+
coop: "same-origin",
|
|
167
|
+
corp: "same-origin",
|
|
168
|
+
permissionsPolicy: "camera=(), microphone=(), geolocation=()",
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
],
|
|
172
|
+
}));
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Les clés sont **typées et auto-complétées** : le module augmente le registre `NodefonyModuleConfig`
|
|
176
|
+
du core (`index.ts:28`), donc `use("@nodefony/security", …)` propose les clés **et** les valeurs
|
|
177
|
+
d'enum (`referrerPolicy`, `coop`, `corp`…). Une valeur hors enum casse le boot, pas la production.
|
|
178
|
+
|
|
179
|
+
### 2. Ce qu'on observe
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Route applicative : socle transport + couche applicative, sur la MÊME réponse.
|
|
183
|
+
curl -sI http://localhost:5151/nodefony/test/index
|
|
184
|
+
|
|
185
|
+
# HTTP/1.1 200 OK
|
|
186
|
+
# X-Content-Type-Options: nosniff ← transport (@nodefony/http)
|
|
187
|
+
# X-Frame-Options: DENY ← transport (@nodefony/http)
|
|
188
|
+
# Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-vZ9…'; …
|
|
189
|
+
# Referrer-Policy: strict-origin-when-cross-origin
|
|
190
|
+
# Cross-Origin-Opener-Policy: same-origin
|
|
191
|
+
# Cross-Origin-Resource-Policy: same-origin
|
|
192
|
+
# Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Le nonce change à CHAQUE requête — deux appels, deux jetons.
|
|
197
|
+
curl -sI http://localhost:5151/nodefony/test/index | grep -o "nonce-[^']*"
|
|
198
|
+
curl -sI http://localhost:5151/nodefony/test/index | grep -o "nonce-[^']*"
|
|
199
|
+
# nonce-2Qk1r0h8… (≠)
|
|
200
|
+
# nonce-Xa7pLd3f…
|
|
201
|
+
|
|
202
|
+
# Une URL inexistante : le socle transport est TOUJOURS là (l'applicatif ne s'exécute pas).
|
|
203
|
+
curl -sI http://localhost:5151/nodefony/test/__inexistant__ | grep -i x-content-type
|
|
204
|
+
# X-Content-Type-Options: nosniff
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 3. Un besoin ponctuel : élargir le CSP d'UNE route
|
|
208
|
+
|
|
209
|
+
Tu dois embarquer une iframe YouTube sur une seule page. Élargir le CSP global serait une faute :
|
|
210
|
+
tu ouvrirais l'ensemble du site. `@Csp` déclare l'écart **à l'échelle de l'action**.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// nodefony/controllers/EmbedController.ts — complet, compile tel quel.
|
|
214
|
+
import { controller, Controller, Get, Csp } from "@nodefony/framework";
|
|
215
|
+
|
|
216
|
+
@controller("/embed")
|
|
217
|
+
class EmbedController extends Controller {
|
|
218
|
+
// `frame-src` n'existe QUE sur cette route ; le reste du site garde le CSP strict.
|
|
219
|
+
@Csp({ "frame-src": ["https://www.youtube.com"] })
|
|
220
|
+
@Get("/video")
|
|
221
|
+
video() {
|
|
222
|
+
return this.renderJson({ embed: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default EmbedController;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
curl -sI http://localhost:5151/embed/video | grep -i content-security-policy
|
|
231
|
+
# … ; frame-src https://www.youtube.com ← ajouté ICI seulement
|
|
232
|
+
curl -sI http://localhost:5151/nodefony/test/index | grep -c youtube
|
|
233
|
+
# 0 ← isolation prouvée (security-headers.test.ts:83)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## 🛡️ Le catalogue des en-têtes
|
|
237
|
+
|
|
238
|
+
Choisir en cinq secondes — puis le détail dans les cartes.
|
|
239
|
+
|
|
240
|
+
| En-tête | Ce qu'il bloque | Défaut Nodefony | Qui l'émet |
|
|
241
|
+
| ------------------------------ | ------------------------------------ | ------------------------------------------ | ------------------- |
|
|
242
|
+
| `Content-Security-Policy` | XSS, injection de source | politique « secure-but-usable » + nonce | security (pipeline) |
|
|
243
|
+
| `X-Frame-Options` | Clickjacking | `DENY` | http (transport) |
|
|
244
|
+
| `X-Content-Type-Options` | MIME sniffing | `nosniff` | http (transport) |
|
|
245
|
+
| `Strict-Transport-Security` | Downgrade HTTPS → HTTP | `max-age=31536000; includeSubDomains`, TLS | http (transport) |
|
|
246
|
+
| `Referrer-Policy` | Fuite d'URL vers des tiers | `no-referrer` | security (pipeline) |
|
|
247
|
+
| `Cross-Origin-Opener-Policy` | Attaques par fenêtre ouvrante | **absent** (opt-in) | security (pipeline) |
|
|
248
|
+
| `Cross-Origin-Embedder-Policy` | Chargement de ressources non signées | **absent** (opt-in) | security (pipeline) |
|
|
249
|
+
| `Cross-Origin-Resource-Policy` | Inclusion de tes assets par un tiers | **absent** (opt-in) | security (pipeline) |
|
|
250
|
+
| `Origin-Agent-Cluster` | Partage de heap entre origines | **absent** (opt-in) | security (pipeline) |
|
|
251
|
+
| `Permissions-Policy` | Accès caméra/micro/géoloc | **absent** (opt-in) | security (pipeline) |
|
|
252
|
+
|
|
253
|
+
### `Content-Security-Policy` — la liste blanche des sources
|
|
254
|
+
|
|
255
|
+
**La menace** : n'importe quelle entrée non échappée (commentaire, nom d'utilisateur, paramètre
|
|
256
|
+
réfléchi) devient un vecteur d'exécution de code. Le CSP est le filet quand l'échappement a raté.
|
|
257
|
+
|
|
258
|
+
**Le défaut Nodefony** est délibérément « secure-but-usable » (`config.ts:218`) : seul `script-src`
|
|
259
|
+
est **strict** — `'self'` plus le nonce de la requête, ce qui est la vraie défense XSS. Le reste
|
|
260
|
+
couvre les besoins réels d'une app moderne (CSS-in-JS via `style-src 'unsafe-inline'`, images
|
|
261
|
+
`data:`/`blob:`, workers, fetch/WS same-origin) et ajoute les durcissements gratuits :
|
|
262
|
+
`object-src 'none'`, `base-uri 'self'`, `form-action 'self'`.
|
|
263
|
+
|
|
264
|
+
**Pourquoi ce compromis** : un CSP qui casse l'application est désactivé par le premier développeur
|
|
265
|
+
pressé. Un CSP strict là où ça compte (`script-src`) et permissif là où ça ne coûte rien (styles,
|
|
266
|
+
images) survit en production — c'est celui-là qui protège vraiment.
|
|
267
|
+
|
|
268
|
+
Le CSP couvre aussi le clickjacking, via `frame-ancestors`, plus finement que `X-Frame-Options`
|
|
269
|
+
(liste d'origines plutôt que tout-ou-rien). Les deux cohabitent : les navigateurs modernes
|
|
270
|
+
privilégient `frame-ancestors`, `X-Frame-Options` reste le filet pour les anciens.
|
|
271
|
+
|
|
272
|
+
### `X-Frame-Options` — non, tu ne m'encadres pas
|
|
273
|
+
|
|
274
|
+
**La menace** : le clickjacking. Ta page est superposée, invisible, à une page appât ; le clic de la
|
|
275
|
+
victime est capté par ton interface.
|
|
276
|
+
|
|
277
|
+
Posé par le **transport** depuis un cache calculé au boot — `secFrameOptions`
|
|
278
|
+
(`http-kernel.ts:270`) — et configuré côté `@nodefony/http` avec `frameOptions`
|
|
279
|
+
(`http/nodefony/config/config.ts:121`), qui vaut `DENY` par défaut. `SAMEORIGIN` si ton propre site
|
|
280
|
+
s'auto-encadre. C'est un des trois en-têtes que security **ne ré-émet pas** : il doit valoir aussi
|
|
281
|
+
pour un HTML statique servi directement depuis `public/`.
|
|
282
|
+
|
|
283
|
+
### `X-Content-Type-Options` — arrête de deviner
|
|
284
|
+
|
|
285
|
+
**La menace** : le MIME sniffing. Un fichier téléversé, servi avec un `Content-Type` imprécis, est
|
|
286
|
+
« deviné » par le navigateur — et un fichier deviné exécutable s'exécute sur **ton** origine, donc
|
|
287
|
+
avec tes cookies.
|
|
288
|
+
|
|
289
|
+
Valeur unique reconnue : `nosniff`, posée depuis le cache `secContentTypeOptions`
|
|
290
|
+
(`http-kernel.ts:1334`). C'est **l'en-tête qui justifie le mieux la couche transport** : le danger
|
|
291
|
+
vient précisément des fichiers servis hors pipeline applicatif — un banc live le prouve sur une 404
|
|
292
|
+
(`security-headers.test.ts:38`).
|
|
293
|
+
|
|
294
|
+
### `Strict-Transport-Security` — HTTPS, et rien d'autre
|
|
295
|
+
|
|
296
|
+
**La menace** : le downgrade. Sur un réseau hostile, la toute première requête en clair suffit à
|
|
297
|
+
installer un intercepteur.
|
|
298
|
+
|
|
299
|
+
La chaîne est assemblée au boot par `HttpKernel.computeSecurityHeaderCaches()`
|
|
300
|
+
(`http-kernel.ts:330`) : `max-age`, puis `includeSubDomains` et `preload` selon la config.
|
|
301
|
+
|
|
302
|
+
Elle n'est posée que **sur une réponse HTTPS ou HTTP/2** — le cache `secHsts` est conditionné au type
|
|
303
|
+
de serveur (`http-kernel.ts:965`). C'est conforme à la RFC 6797, qui veut qu'un HSTS reçu en clair
|
|
304
|
+
soit ignoré : l'émettre sur du HTTP simple ne ferait que polluer. Défaut : un an, sous-domaines
|
|
305
|
+
inclus.
|
|
306
|
+
|
|
307
|
+
> [!CAUTION]
|
|
308
|
+
> `preload: true` (`http/nodefony/config/config.ts:92`) inscrit ton domaine dans la liste
|
|
309
|
+
> pré-chargée des navigateurs. C'est un **engagement quasi irréversible** : tout sous-domaine
|
|
310
|
+
> incapable de servir en HTTPS devient inaccessible, et la sortie de liste prend des mois. À ne
|
|
311
|
+
> jamais activer « pour voir ».
|
|
312
|
+
|
|
313
|
+
### `Referrer-Policy` — ne raconte pas d'où tu viens
|
|
314
|
+
|
|
315
|
+
**La menace** : la fuite d'URL. Chemins parlants, identifiants de session dans une query, jetons de
|
|
316
|
+
réinitialisation — tout part chez le site suivant via le `Referer`.
|
|
317
|
+
|
|
318
|
+
Défaut Nodefony : `no-referrer` (`security/nodefony/config/config.ts:263`), la valeur la plus stricte. La valeur est un
|
|
319
|
+
**enum W3C fermé** — huit valeurs validées au boot, donc pas de faute de frappe qui passerait en
|
|
320
|
+
silence (l'écriture libre `no-refferer` casserait la protection sans prévenir).
|
|
321
|
+
|
|
322
|
+
Le choix usuel pour un site public reste `strict-origin-when-cross-origin` : URL complète en
|
|
323
|
+
interne, origine seule vers l'extérieur, rien du tout vers du HTTP en clair.
|
|
324
|
+
|
|
325
|
+
### `Cross-Origin-Opener-Policy` — coupe le lien avec la fenêtre ouvrante
|
|
326
|
+
|
|
327
|
+
**La menace** : une page ouverte par la tienne (ou qui t'a ouverte) garde une référence
|
|
328
|
+
`window.opener` et partage un groupe de contexte de navigation — surface d'attaque pour du
|
|
329
|
+
_tabnabbing_ et pour les canaux auxiliaires type Spectre.
|
|
330
|
+
|
|
331
|
+
`same-origin` (`securityHeaders.ts:71`) rompt ce lien. C'est aussi, avec COEP, l'une des deux
|
|
332
|
+
conditions de l'**isolation cross-origin**, indispensable si tu veux `SharedArrayBuffer` ou des
|
|
333
|
+
timers haute résolution.
|
|
334
|
+
|
|
335
|
+
### `Cross-Origin-Embedder-Policy` — je n'embarque que du consenti
|
|
336
|
+
|
|
337
|
+
**La menace** : ta page embarque des ressources tierces qui n'ont jamais donné leur accord, et les
|
|
338
|
+
place dans ton processus.
|
|
339
|
+
|
|
340
|
+
`require-corp` (`securityHeaders.ts:72`) exige que **chaque** ressource tierce s'annonce comme
|
|
341
|
+
partageable (CORP ou CORS). C'est le complément de COOP pour l'isolation complète.
|
|
342
|
+
|
|
343
|
+
> [!WARNING]
|
|
344
|
+
> `coep: "require-corp"` **casse toutes les ressources tierces non conformes** — polices Google,
|
|
345
|
+
> images de CDN, iframes de paiement. C'est pour cette raison qu'il est absent des défauts, et
|
|
346
|
+
> volontairement exclu du banc de test (`security-headers.test.ts:62`). À activer seulement si tu
|
|
347
|
+
> as besoin de l'isolation cross-origin, et après audit de tes assets.
|
|
348
|
+
|
|
349
|
+
### `Cross-Origin-Resource-Policy` — mes assets ne s'incluent pas ailleurs
|
|
350
|
+
|
|
351
|
+
**La menace** : symétrique du précédent. Un site tiers inclut tes images ou tes scripts pour les
|
|
352
|
+
mesurer, les mettre en cache, ou monter une attaque par inclusion.
|
|
353
|
+
|
|
354
|
+
`same-origin` (`securityHeaders.ts:73`) interdit toute inclusion externe ; `same-site` autorise tes
|
|
355
|
+
propres sous-domaines ; `cross-origin` ouvre — c'est ce qu'il faut sur une CDN publique assumée.
|
|
356
|
+
|
|
357
|
+
### `Origin-Agent-Cluster` — un bac à sable par origine
|
|
358
|
+
|
|
359
|
+
**La menace** : plusieurs origines partageant heap et processus, donc des canaux auxiliaires
|
|
360
|
+
mesurables.
|
|
361
|
+
|
|
362
|
+
Nodefony l'émet comme un **booléen de champ structuré** RFC 8941 : la valeur est littéralement `?1`
|
|
363
|
+
(`securityHeaders.ts:75`). C'est une **demande**, pas une garantie — le navigateur décide.
|
|
364
|
+
|
|
365
|
+
### `Permissions-Policy` — coupe le micro par défaut
|
|
366
|
+
|
|
367
|
+
**La menace** : une iframe tierce (widget, publicité) demande la caméra, le micro ou la position, et
|
|
368
|
+
la boîte de dialogue s'affiche sous **ton** nom de domaine.
|
|
369
|
+
|
|
370
|
+
Valeur libre — le champ `permissionsPolicy` est recopié tel quel (`securityHeaders.ts:76`),
|
|
371
|
+
typiquement `camera=(), microphone=(), geolocation=()` : la
|
|
372
|
+
liste vide signifie « personne, pas même moi ». Absent par défaut car la liste des fonctionnalités
|
|
373
|
+
dépend entièrement de l'application.
|
|
374
|
+
|
|
375
|
+
## ⚙️ Configuration — deux sections, deux modules
|
|
376
|
+
|
|
377
|
+
Réflexe à acquérir : **le nom du module dit qui pose l'en-tête**. Chercher `frameOptions` dans la
|
|
378
|
+
config security est la première source de confusion sur ce sujet.
|
|
379
|
+
|
|
380
|
+
### Couche applicative — `use("@nodefony/security", { headers })`
|
|
381
|
+
|
|
382
|
+
Dérivé du schéma Zod `headersSchema` (`config.ts:194`).
|
|
383
|
+
|
|
384
|
+
<!-- prettier-ignore -->
|
|
385
|
+
| Option | Type | Défaut | Effet |
|
|
386
|
+
| --- | --- | --- | --- |
|
|
387
|
+
| `enabled` | booléen | `true` | Coupe toute la couche applicative. |
|
|
388
|
+
| `csp` | chaîne | politique « secure-but-usable » | Valeur de `Content-Security-Policy`. |
|
|
389
|
+
| `cspNonces` | booléen | `true` | Active la substitution de `{{nonce}}` par requête. |
|
|
390
|
+
| `referrerPolicy` | enum W3C (8 valeurs) | `no-referrer` | Valeur de `Referrer-Policy`. |
|
|
391
|
+
| `coop` | enum, optionnel | absent | `Cross-Origin-Opener-Policy`. |
|
|
392
|
+
| `coep` | enum, optionnel | absent | `Cross-Origin-Embedder-Policy`. |
|
|
393
|
+
| `corp` | enum, optionnel | absent | `Cross-Origin-Resource-Policy`. |
|
|
394
|
+
| `originAgentCluster` | booléen, optionnel | absent | Émet `Origin-Agent-Cluster: ?1`. |
|
|
395
|
+
| `permissionsPolicy` | chaîne, optionnelle | absent | Valeur de `Permissions-Policy`. |
|
|
396
|
+
|
|
397
|
+
### Socle transport — `use("@nodefony/http", { securityHeaders })`
|
|
398
|
+
|
|
399
|
+
Dérivé de `securityHeadersSchema` (`http/nodefony/config/config.ts:108`). Ces trois réglages sont
|
|
400
|
+
**éditables à chaud** (`runtimeMutable`) : `HttpKernel.onConfigChanged()` (`http-kernel.ts:290`)
|
|
401
|
+
recalcule les caches, donc la valeur suivante s'applique sans redémarrage.
|
|
402
|
+
|
|
403
|
+
| Option | Type | Défaut | Effet |
|
|
404
|
+
| ------------------------------------------- | ----------------- | ---------- | ------------------------------------------------------------------ |
|
|
405
|
+
| `contentTypeOptions` | chaîne ou `null` | `nosniff` | `X-Content-Type-Options` ; `null` = ne pas émettre. |
|
|
406
|
+
| `frameOptions` | chaîne ou `null` | `DENY` | `X-Frame-Options` ; `SAMEORIGIN` si auto-encadrement. |
|
|
407
|
+
| `strictTransportSecurity` | objet ou `null` | activé | `null` = pas de HSTS du tout. |
|
|
408
|
+
| `strictTransportSecurity.maxAge` | entier (secondes) | `31536000` | Durée mémorisée par le navigateur (un an, recommandé OWASP). |
|
|
409
|
+
| `strictTransportSecurity.includeSubDomains` | booléen | `true` | Étend la contrainte à tous les sous-domaines. |
|
|
410
|
+
| `strictTransportSecurity.preload` | booléen | `false` | Inscription à la liste pré-chargée — **irréversible en pratique**. |
|
|
411
|
+
|
|
412
|
+
> [!WARNING]
|
|
413
|
+
> Les clés `hsts`, `hstsMaxAgeS`, `frameguard` et `noSniff` **existent** dans la config security
|
|
414
|
+
> (`config.ts:200`, `config.ts:227`, `config.ts:233`) mais **ne pilotent rien** : la couche
|
|
415
|
+
> applicative ne les lit pas (`securityHeaders.ts:6`), elles ne servent qu'à l'introspection
|
|
416
|
+
> affichée dans Studio. Pour changer réellement `X-Frame-Options`, c'est `securityHeaders.frameOptions`
|
|
417
|
+
> **du module http**. Même remarque pour `hidePoweredBy` (`config.ts:254`) : Nodefony n'émet aucun
|
|
418
|
+
> `X-Powered-By`, l'option est un no-op documenté.
|
|
419
|
+
|
|
420
|
+
## 🏗️ Le CSP en détail — deux régimes, trois façons de l'étendre
|
|
421
|
+
|
|
422
|
+
### Régime 1 — CSP statique (0 allocation par requête)
|
|
423
|
+
|
|
424
|
+
Si le CSP ne contient pas `{{nonce}}`, ou si `cspNonces` est à `false`, la chaîne est rangée telle
|
|
425
|
+
quelle dans la table gelée du boot (`securityHeaders.ts:66`). Par requête : une lecture, un
|
|
426
|
+
`setHeader`. Rien d'autre.
|
|
427
|
+
|
|
428
|
+
Détail de robustesse : si tu désactives `cspNonces` en laissant le placeholder dans la chaîne, le
|
|
429
|
+
token résiduel `'nonce-{{nonce}}'` est **purgé** (`securityHeaders.ts:63`). Sans ça, tu servirais un
|
|
430
|
+
CSP contenant un nonce littéral jamais émis — donc un `script-src` qui bloque **tout**, y compris tes
|
|
431
|
+
propres scripts. Le code refuse de produire un CSP cassé.
|
|
432
|
+
|
|
433
|
+
### Régime 2 — nonce par requête (la vraie défense anti-XSS)
|
|
434
|
+
|
|
435
|
+
Un nonce autorise **l'inline que tu as toi-même rendu**, et lui seul. Un script injecté par un
|
|
436
|
+
attaquant ne peut pas deviner la valeur : il est refusé même s'il est syntaxiquement identique.
|
|
437
|
+
|
|
438
|
+
Le chemin complet, sans surprise :
|
|
439
|
+
|
|
440
|
+
1. **Au boot**, la chaîne CSP est **pré-découpée** autour de `{{nonce}}` (`securityHeaders.ts:58`).
|
|
441
|
+
Aucun parsing ni regex n'aura lieu pendant une requête.
|
|
442
|
+
2. **Par requête**, `Firewall.applySecurityHeaders()` (`firewall.ts:835`) lit `context.cspNonce` —
|
|
443
|
+
ce qui **génère** le jeton à cet instant (`Context.ts:253`) — puis appelle
|
|
444
|
+
`SecurityHeaders.cspFor()` (`securityHeaders.ts:100`) : un seul `join`.
|
|
445
|
+
3. **Dans la vue**, le contrôleur relit `context.cspNonce`, qui est **mémoïsé** : l'en-tête et le
|
|
446
|
+
`<script nonce="…">` portent forcément la même valeur. C'est le motif employé par le contrôleur
|
|
447
|
+
de Studio (`StudioController.ts:62`).
|
|
448
|
+
|
|
449
|
+
`SecurityHeaders.hasNonce` (`securityHeaders.ts:88`) est le drapeau qui décide du régime : à `false`,
|
|
450
|
+
pas une seule opération crypto. Il n'y a **aucun setter** pour `cspNonce` : un jeton serveur doit
|
|
451
|
+
rester imprévisible, jamais pilotable par le client — contrairement au `requestId`, qui, lui, accepte
|
|
452
|
+
une corrélation entrante.
|
|
453
|
+
|
|
454
|
+
**Placement dans le pipeline** : `applySecurityHeaders` est appelé **après le resolve** et **avant**
|
|
455
|
+
le repli statique et le `writeHead` (`http-kernel.ts:1334`). Cet ordre n'est pas cosmétique : il
|
|
456
|
+
faut que le routeur ait posé les directives `@Csp` de la route pour pouvoir les fusionner, et il faut
|
|
457
|
+
être avant l'écriture des en-têtes pour pouvoir en poser.
|
|
458
|
+
|
|
459
|
+
### Étendre le CSP — trois portées, une seule mécanique
|
|
460
|
+
|
|
461
|
+
| Portée | Outil | Pour quoi | Recalculé |
|
|
462
|
+
| ------------------ | ------------------------------- | ----------------------------------------------- | ------------------- |
|
|
463
|
+
| Application | `headers.csp` en config | ta politique de base | au boot |
|
|
464
|
+
| Module | `Firewall.registerCspOrigins()` | besoin **permanent** d'un module (ex. Vite) | à l'enregistrement |
|
|
465
|
+
| Route / contrôleur | `@Csp({ … })` | besoin **ponctuel** d'une réponse (iframe, CDN) | par requête décorée |
|
|
466
|
+
|
|
467
|
+
Les trois convergent vers `mergeCspFragments()` (`csp.ts:56`), et c'est un **merge structuré**, pas
|
|
468
|
+
une concaténation. Pourquoi c'est vital : en CSP, une directive **répétée est ignorée** après sa
|
|
469
|
+
première occurrence (W3C CSP Level 3 §3, rationnel documenté `csp.ts:8`). Concaténer
|
|
470
|
+
`"script-src 'self'"` et `"script-src https://cdn"` produirait un en-tête où la seconde est purement
|
|
471
|
+
et simplement jetée — une extension silencieusement sans effet, le pire des deux mondes.
|
|
472
|
+
|
|
473
|
+
Le merge fusionne donc les sources **dans une seule directive**, dédoublonnées, base d'abord ; une
|
|
474
|
+
directive absente est ajoutée en fin. La fonction est **pure et déterministe** (`parseCsp()`
|
|
475
|
+
`csp.ts:25` → `serializeCsp()` `csp.ts:34`), ce qui rend l'en-tête stable d'une requête à l'autre et
|
|
476
|
+
les tests fiables.
|
|
477
|
+
|
|
478
|
+
**Coût** : le merge d'un module est payé **une fois**, au (dés)enregistrement
|
|
479
|
+
(`Firewall.#rebuildSecurityHeaders()`, `firewall.ts:1085`), jamais par requête. Le merge d'une route
|
|
480
|
+
`@Csp` est payé **uniquement sur les routes décorées** (`SecurityHeaders.cspForExtra()`,
|
|
481
|
+
`securityHeaders.ts:115`) ; le cas courant reste le simple `join`.
|
|
482
|
+
|
|
483
|
+
## 🧩 Extension — déclarer un fragment CSP depuis son module
|
|
484
|
+
|
|
485
|
+
Un module qui a besoin d'origines supplémentaires ne doit **jamais** poser l'en-tête lui-même : il en
|
|
486
|
+
émettrait un second, et le navigateur applique alors l'intersection la plus stricte — au mieux
|
|
487
|
+
inefficace, au pire il casse la page. Il **déclare** son besoin, security fusionne.
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// Dans le service d'un module — le firewall est résolu PAR NOM (aucun import de security).
|
|
491
|
+
const firewall = this.container?.get?.("firewall") as
|
|
492
|
+
| { registerCspOrigins?(m: string, f: Record<string, string[]>): void }
|
|
493
|
+
| undefined;
|
|
494
|
+
|
|
495
|
+
firewall?.registerCspOrigins?.("mon-module", {
|
|
496
|
+
"connect-src": ["https://api.partenaire.tld"],
|
|
497
|
+
"img-src": ["https://cdn.partenaire.tld"],
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Trois propriétés à retenir :
|
|
502
|
+
|
|
503
|
+
- **Aucun couplage** : la résolution par nom de service évite un cycle de dépendances, et
|
|
504
|
+
`registerCspOrigins` est optionnel — un module fonctionne dans une app **sans** security.
|
|
505
|
+
- **Réversible** : `Firewall.unregisterCspOrigins()` (`firewall.ts:1074`) retire le fragment et
|
|
506
|
+
reconstruit le CSP de base. C'est ce que fait `@nodefony/frontend` à l'arrêt du serveur Vite.
|
|
507
|
+
- **Idempotent** : la reconstruction repart **toujours** du `headers.csp` d'origine
|
|
508
|
+
(`firewall.ts:1088`), jamais d'un CSP déjà fusionné — pas d'accumulation entre deux
|
|
509
|
+
enregistrements.
|
|
510
|
+
|
|
511
|
+
L'exemple de référence vit dans le framework : en développement, `@nodefony/frontend` déclare les
|
|
512
|
+
origines du serveur Vite et `'unsafe-eval'` (exigé par le Fast Refresh de React) via
|
|
513
|
+
`FrontendService.#viteCspFragment()` (`FrontendService.ts:909`) — ce qui explique qu'un CSP observé
|
|
514
|
+
en dev soit plus large qu'en production, où ce fragment n'existe pas.
|
|
515
|
+
|
|
516
|
+
## 📜 Normes appliquées
|
|
517
|
+
|
|
518
|
+
| Domaine | Norme | Ancrage dans le code |
|
|
519
|
+
| ------------------------------------ | -------------------------------- | ------------------------------------------------------------ |
|
|
520
|
+
| Politique de sécurité du contenu | W3C CSP Level 3 | `mergeCspFragments()` (`csp.ts:56`), directive non dupliquée |
|
|
521
|
+
| Nonce CSP (unicité, imprévisibilité) | W3C CSP Level 3 §6.7.4 | `Context.cspNonce` — 128 bits CSPRNG (`Context.ts:253`) |
|
|
522
|
+
| HSTS | RFC 6797 | posé sur TLS uniquement (`http-kernel.ts:839`) |
|
|
523
|
+
| Champ structuré booléen | RFC 8941 | `Origin-Agent-Cluster: ?1` (`securityHeaders.ts:75`) |
|
|
524
|
+
| Referrer-Policy | W3C Referrer Policy (enum fermé) | 8 valeurs validées au boot (`config.ts:239`) |
|
|
525
|
+
| Isolation cross-origin | WHATWG HTML (COOP/COEP/CORP) | `securityHeaders.ts:71` |
|
|
526
|
+
| Anti-MIME-sniffing | WHATWG Fetch (`nosniff`) | `secContentTypeOptions` (`http-kernel.ts:1334`) |
|
|
527
|
+
| Durcissement en-têtes | OWASP Secure Headers | `computeSecurityHeaderCaches()` (`http-kernel.ts:330`) |
|
|
528
|
+
|
|
529
|
+
## ⚡ Performance & mémoire
|
|
530
|
+
|
|
531
|
+
Le coût est concentré au boot, par construction :
|
|
532
|
+
|
|
533
|
+
- **En-têtes constants** : une seule table, gelée (`securityHeaders.ts:77`). Par requête, une boucle
|
|
534
|
+
`for…in` sur un objet de 1 à 6 entrées et autant de `setHeader`. Aucune allocation.
|
|
535
|
+
- **CSP statique** : rien de plus — la chaîne est dans la table.
|
|
536
|
+
- **CSP à nonce** : `randomBytes(16)` plus un `join` par requête. C'est le seul coût variable, et il
|
|
537
|
+
n'existe **que** si le CSP porte un placeholder : `hasNonce` (`securityHeaders.ts:88`)
|
|
538
|
+
court-circuite entièrement ce chemin sinon. La paresse de `Context.cspNonce` (`Context.ts:253`)
|
|
539
|
+
protège en plus les chemins internes qui n'atteignent jamais le firewall.
|
|
540
|
+
- **Merge CSP** : jamais dans le chemin chaud. Le fragment d'un module est fusionné à
|
|
541
|
+
l'enregistrement (`firewall.ts:1067`) ; celui d'une route ne coûte que sur les routes `@Csp`.
|
|
542
|
+
- **Socle transport** : trois `setHeader` sur des chaînes précalculées (`http-kernel.ts:1334`), avec
|
|
543
|
+
un test `!== null` qui annule le coût des en-têtes désactivés.
|
|
544
|
+
|
|
545
|
+
Le module n'attache aucun écouteur d'événement et ne conserve aucun état par requête : il n'entre pas
|
|
546
|
+
dans le périmètre du gate mémoire, qu'il ne peut structurellement pas dégrader.
|
|
547
|
+
|
|
548
|
+
## 📡 Observabilité — Studio
|
|
549
|
+
|
|
550
|
+
L'écran **Firewall** de Studio affiche la section « En-têtes de sécurité » — pilotée par
|
|
551
|
+
`headers.enabled` (`FirewallDefenses.tsx:219`) — avec le CSP effectif, l'état du nonce par requête, la
|
|
552
|
+
Referrer-Policy et les valeurs d'isolation. Les données
|
|
553
|
+
viennent de `Firewall.describe()` (`firewall.ts:505`), qui projette la config **sans aucun secret**,
|
|
554
|
+
exposée par `GET /nodefony/security/api/firewall`.
|
|
555
|
+
|
|
556
|
+
L'onglet **Configuration** de Studio rend les mêmes options depuis le schéma Zod — chaque champ y
|
|
557
|
+
porte sa description, ce qui en fait la référence toujours à jour des défauts.
|
|
558
|
+
|
|
559
|
+
> [!NOTE]
|
|
560
|
+
> La ligne « frameguard » de cet écran reflète la **config security**, pas la valeur réellement émise
|
|
561
|
+
> par le transport. La source de vérité pour `X-Frame-Options`, c'est la réponse HTTP elle-même :
|
|
562
|
+
> `curl -I` tranche en une seconde.
|
|
563
|
+
|
|
564
|
+
## ⚠️ Pièges (symptôme → cause → correction)
|
|
565
|
+
|
|
566
|
+
| Symptôme | Cause dans le code | Correction |
|
|
567
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
568
|
+
| `frameguard: "sameorigin"` en config security sans effet | Clé **inerte** — non lue par la couche applicative (`securityHeaders.ts:6`) | Régler `securityHeaders.frameOptions` du module **http** |
|
|
569
|
+
| `<script>` inline bloqué | Le template ne reprend pas le nonce | Rendre `<script nonce="…">` depuis `context.cspNonce` |
|
|
570
|
+
| CSP contenant `'nonce-{{nonce}}'` littéral | `cspNonces` désactivé, placeholder laissé | Nodefony purge le résiduel (`securityHeaders.ts:63`) ; retirer le placeholder |
|
|
571
|
+
| Une extension `script-src` de module sans effet | Second en-tête / directive dupliquée (ignorée, W3C CSP3 §3) | Déclarer un fragment via `registerCspOrigins()`, jamais un `setHeader` |
|
|
572
|
+
| Assets tiers cassés après activation de l'isolation | `coep: "require-corp"` exige CORP/CORS sur **chaque** ressource | Retirer `coep` ou faire annoncer les ressources |
|
|
573
|
+
| HSTS absent en développement | Posé sur TLS uniquement (`http-kernel.ts:839`) | Comportement conforme RFC 6797 — vérifier sur le port HTTPS |
|
|
574
|
+
| Le CSP est plus large en dev qu'en prod | `@nodefony/frontend` déclare les origines Vite (`FrontendService.ts:695`) | Attendu : le fragment n'existe pas en production |
|
|
575
|
+
| Aucun en-tête applicatif | Module security absent ou `headers.enabled: false` | Le socle transport reste actif ; réactiver security pour CSP/Referrer/isolation |
|
|
576
|
+
| Domaine injoignable après activation de `preload` | Inscription à la liste pré-chargée, sortie très lente | Ne jamais activer sans plan HTTPS sur **tous** les sous-domaines |
|
|
577
|
+
|
|
578
|
+
## 🧪 Tests & couverture
|
|
579
|
+
|
|
580
|
+
Trois familles couvrent la brique — les compteurs exacts vivent dans la carte de l'aperçu, régénérée
|
|
581
|
+
depuis vitest :
|
|
582
|
+
|
|
583
|
+
- **Unitaires** (`@nodefony/security`) — `securityHeaders.test` : la table figée, la séparation
|
|
584
|
+
transport/applicatif prouvée par l'absence des trois en-têtes transport, les avancés opt-in, les
|
|
585
|
+
deux régimes CSP, la purge du résiduel, et `cspForExtra` (fusion, ajout, substitution du nonce) ;
|
|
586
|
+
`csp.test` : parse, merge et sérialisation des fragments.
|
|
587
|
+
- **Intégration sur serveur réel** (`@nodefony/http`, port 5151) — `security-headers.test` vérifie les
|
|
588
|
+
deux couches sur une **vraie** réponse. Quatre invariants y sont prouvés :
|
|
589
|
+
- le socle transport survit à une 404, avec `x-content-type-options` sur une route inexistante
|
|
590
|
+
(`security-headers.test.ts:38`) ;
|
|
591
|
+
- le CSP applicatif complète cette réponse avec `content-security-policy`
|
|
592
|
+
(`security-headers.test.ts:43`) ;
|
|
593
|
+
- une route décorée voit sa directive `frame-src` fusionnée, sans dupliquer `img-src`
|
|
594
|
+
(`security-headers.test.ts:73`) ;
|
|
595
|
+
- deux requêtes concurrentes reçoivent deux nonces différents (`security-headers.test.ts:100`).
|
|
596
|
+
|
|
597
|
+
`headers.test` et `security.test` couvrent le reste du contrat d'en-têtes.
|
|
598
|
+
|
|
599
|
+
- **Absent, assumé** : pas de banc d'**attaque** dédié à cette brique (contrairement à CSRF, CORS ou
|
|
600
|
+
l'autorisation, qui ont leur `*.attack.test.ts`), et pas de test de **charge** propre — le coût est
|
|
601
|
+
structurellement nul par requête, et le pipeline complet est déjà couvert par le gate mémoire de
|
|
602
|
+
`@nodefony/http`.
|
|
603
|
+
|
|
604
|
+
Couverture : `npm run coverage` dans `@nodefony/security`. Revue de sécurité d'un diff touchant cette
|
|
605
|
+
brique : skill `nodefony-security-review`.
|
|
606
|
+
|
|
607
|
+
## 🔗 Pour aller plus loin
|
|
608
|
+
|
|
609
|
+
- ⬆️ **Retour au hub** : [Sécurité — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
610
|
+
- 🧭 **Pages sœurs** : [CORS](cors.md) · [CSRF](csrf.md)
|
|
611
|
+
|
|
612
|
+
- Le pare-feu qui pose la couche applicative → [firewall](./firewall.md)
|
|
613
|
+
- CORS, l'autre famille d'en-têtes (`Access-Control-*`) → [cors](./cors.md)
|
|
614
|
+
- CSRF, la défense complémentaire contre les requêtes forcées → [csrf](./csrf.md)
|
|
615
|
+
- Vue d'ensemble du module → [index](./index.md)
|
|
616
|
+
- Où les deux couches s'insèrent dans le pipeline → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)
|