@nodefony/security 10.0.0-beta.2 → 10.0.0-beta.4
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/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +48 -16
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
- package/dist/nodefony/command/security-oauth-doctor.js +68 -0
- package/dist/nodefony/command/security-secrets.js +52 -29
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +10 -7
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +9 -14
- package/dist/nodefony/config/config.js +99 -8
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/contracts/index.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/errors/index.js +2 -1
- package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
- package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
- package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
- package/dist/nodefony/keycloak/scaffold.js +231 -0
- package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
- package/dist/nodefony/scaffold/contribute.js +19 -0
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +118 -16
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +116 -25
- package/dist/nodefony/service/keycloak.js +208 -0
- package/dist/nodefony/service/oauth2.js +403 -28
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +94 -27
- package/dist/nodefony/service/totp.js +55 -11
- package/dist/nodefony/service/webAuthn.js +24 -7
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +15 -5
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -3
- package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
- package/dist/nodefony/src/authenticator/bearer.js +1 -2
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +38 -0
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +28 -12
- package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
- package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
- package/dist/nodefony/src/oauth/providerRoles.js +78 -0
- package/dist/nodefony/src/oauth/providers/github.js +48 -12
- package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
- package/dist/nodefony/src/openToSignup.js +46 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +25 -0
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +89 -48
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +60 -4
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/types/index.d.ts +26 -6
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
- package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
- package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
- package/dist/types/nodefony/config/config.d.ts +64 -1
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
- package/dist/types/nodefony/errors/index.d.ts +1 -0
- package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
- package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
- package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
- package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
- package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/authFlow.d.ts +35 -3
- package/dist/types/nodefony/service/firewall.d.ts +17 -3
- package/dist/types/nodefony/service/keycloak.d.ts +115 -0
- package/dist/types/nodefony/service/oauth2.d.ts +145 -9
- package/dist/types/nodefony/service/tokenService.d.ts +7 -1
- package/dist/types/nodefony/service/totp.d.ts +28 -4
- package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
- package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
- package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
- package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
- package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
- package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
- package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
- package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
- package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
- package/docs/api-keys.md +182 -175
- package/docs/audit.md +141 -141
- package/docs/authenticators.md +129 -96
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +77 -72
- package/docs/external-jwt.md +83 -11
- package/docs/firewall.md +119 -83
- package/docs/headers.md +82 -82
- package/docs/index.md +16 -8
- package/docs/keycloak.md +835 -0
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +291 -166
- package/docs/obtenir-un-jeton.md +131 -34
- package/docs/tokens.md +197 -140
- package/docs/totp.md +177 -136
- package/docs/webauthn.md +157 -154
- package/docs/webhooks.md +128 -125
- package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/account/theme.properties +14 -0
- package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/admin/theme.properties +12 -0
- package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
- package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
- package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
- package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/email/theme.properties +8 -0
- package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
- package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
- package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
- package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
- package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/login/template.ftl +423 -0
- package/keycloak/themes/nodefony/login/theme.properties +17 -0
- package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
- package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
- package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
- package/package.json +19 -17
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/headers.md
CHANGED
|
@@ -38,8 +38,8 @@ source: "src/packages/@nodefony/security/docs/headers.md"
|
|
|
38
38
|
> les pose en **deux couches, une seule autorité par en-tête** : le socle **transport**
|
|
39
39
|
> (`@nodefony/http`, dès l'entrée brute — couvre aussi les fichiers statiques et les erreurs) et la
|
|
40
40
|
> couche **applicative** (`@nodefony/security`, dans le pipeline — CSP, Referrer-Policy, isolation
|
|
41
|
-
> cross-origin). Ancré sur `SecurityHeaders` (`securityHeaders.ts
|
|
42
|
-
> `Firewall.applySecurityHeaders()` (`firewall.ts
|
|
41
|
+
> cross-origin). Ancré sur `SecurityHeaders` (`securityHeaders.ts#SecurityHeaders`) et
|
|
42
|
+
> `Firewall.applySecurityHeaders()` (`firewall.ts#Firewall.applySecurityHeaders`).
|
|
43
43
|
|
|
44
44
|
📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **En-têtes de sécurité**
|
|
45
45
|
|
|
@@ -67,7 +67,7 @@ flowchart TD
|
|
|
67
67
|
|
|
68
68
|
**Une seule source par en-tête** : `@nodefony/security` ne ré-émet **jamais** les trois en-têtes
|
|
69
69
|
transport — c'est écrit noir sur blanc dans le contrat de la couche applicative
|
|
70
|
-
(`ISecurityHeadersOptions`, `securityHeaders.ts
|
|
70
|
+
(`ISecurityHeadersOptions`, `securityHeaders.ts#ISecurityHeadersOptions`). Pas de double émission, donc pas de valeurs
|
|
71
71
|
contradictoires sur la même réponse.
|
|
72
72
|
|
|
73
73
|
## 📖 Lexique
|
|
@@ -117,20 +117,20 @@ Le détail de chaque en-tête — menace, valeur par défaut, compromis — est
|
|
|
117
117
|
Un framework qui recalcule ses en-têtes à chaque requête paie ce confort en allocations. Nodefony
|
|
118
118
|
fait l'inverse : **tout ce qui est constant est calculé une fois au démarrage**.
|
|
119
119
|
|
|
120
|
-
- `SecurityHeaders` (`securityHeaders.ts
|
|
120
|
+
- `SecurityHeaders` (`securityHeaders.ts#SecurityHeaders`) construit **au boot** la table des en-têtes constants
|
|
121
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
|
|
122
|
+
statique) et la **gèle** avec `Object.freeze` (`securityHeaders.ts#SecurityHeaders.constructor`). Par requête, le firewall
|
|
123
123
|
se contente de la parcourir et de la poser : zéro concaténation, zéro objet créé.
|
|
124
124
|
- Côté transport, même principe : `HttpKernel.computeSecurityHeaderCaches()`
|
|
125
|
-
(`http-kernel.ts
|
|
126
|
-
boot ; `onHttpRequest` (`http-kernel.ts
|
|
125
|
+
(`http-kernel.ts#HttpKernel.computeSecurityHeaderCaches`) précalcule la chaîne HSTS (`max-age`, `includeSubDomains`, `preload`) au
|
|
126
|
+
boot ; `onHttpRequest` (`http-kernel.ts#HttpKernel.onHttpRequest`) ne fait plus que trois `setHeader`.
|
|
127
127
|
- Le seul coût variable est le **nonce CSP**, et il est **paresseux** : `Context.cspNonce`
|
|
128
|
-
(`Context.ts
|
|
128
|
+
(`Context.ts#Context.#cspNonce`) ne tire ses 128 bits (16 octets CSPRNG, en base64) d'un pool amorti (`randomFillSync`, 4 Ko pour 256 nonces) qu'à la première lecture,
|
|
129
129
|
puis mémoïse. Une réponse qui n'a aucun script inline à signer ne paie aucun appel crypto.
|
|
130
130
|
|
|
131
131
|
Le second parti pris est la **séparation d'autorité** décrite plus haut : un seul émetteur par
|
|
132
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
|
|
133
|
+
même réponse (`security-headers.test.ts`).
|
|
134
134
|
|
|
135
135
|
> [!IMPORTANT]
|
|
136
136
|
> `@nodefony/security` est **optionnel**, pas le socle. Une app Nodefony sans module security émet
|
|
@@ -173,7 +173,7 @@ export default defineConfig(() => ({
|
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
Les clés sont **typées et auto-complétées** : le module augmente le registre `NodefonyModuleConfig`
|
|
176
|
-
du core (`index.ts
|
|
176
|
+
du core (`index.ts#nodefony`), donc `use("@nodefony/security", …)` propose les clés **et** les valeurs
|
|
177
177
|
d'enum (`referrerPolicy`, `coop`, `corp`…). Une valeur hors enum casse le boot, pas la production.
|
|
178
178
|
|
|
179
179
|
### 2. Ce qu'on observe
|
|
@@ -230,7 +230,7 @@ export default EmbedController;
|
|
|
230
230
|
curl -sI http://localhost:5151/embed/video | grep -i content-security-policy
|
|
231
231
|
# … ; frame-src https://www.youtube.com ← ajouté ICI seulement
|
|
232
232
|
curl -sI http://localhost:5151/nodefony/test/index | grep -c youtube
|
|
233
|
-
# 0 ← isolation prouvée (security-headers.test.ts
|
|
233
|
+
# 0 ← isolation prouvée (security-headers.test.ts)
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
## 🛡️ Le catalogue des en-têtes
|
|
@@ -255,7 +255,7 @@ Choisir en cinq secondes — puis le détail dans les cartes.
|
|
|
255
255
|
**La menace** : n'importe quelle entrée non échappée (commentaire, nom d'utilisateur, paramètre
|
|
256
256
|
réfléchi) devient un vecteur d'exécution de code. Le CSP est le filet quand l'échappement a raté.
|
|
257
257
|
|
|
258
|
-
**Le défaut Nodefony** est délibérément « secure-but-usable » (`config.ts
|
|
258
|
+
**Le défaut Nodefony** est délibérément « secure-but-usable » (`config.ts#headersSchema`) : seul `script-src`
|
|
259
259
|
est **strict** — `'self'` plus le nonce de la requête, ce qui est la vraie défense XSS. Le reste
|
|
260
260
|
couvre les besoins réels d'une app moderne (CSS-in-JS via `style-src 'unsafe-inline'`, images
|
|
261
261
|
`data:`/`blob:`, workers, fetch/WS same-origin) et ajoute les durcissements gratuits :
|
|
@@ -275,8 +275,8 @@ privilégient `frame-ancestors`, `X-Frame-Options` reste le filet pour les ancie
|
|
|
275
275
|
victime est capté par ton interface.
|
|
276
276
|
|
|
277
277
|
Posé par le **transport** depuis un cache calculé au boot — `secFrameOptions`
|
|
278
|
-
(`http-kernel.ts
|
|
279
|
-
(`http/nodefony/config/config.ts
|
|
278
|
+
(`http-kernel.ts#HttpKernel.secFrameOptions`) — et configuré côté `@nodefony/http` avec `frameOptions`
|
|
279
|
+
(`http/nodefony/config/config.ts#securityHeadersSchema`), qui vaut `DENY` par défaut. `SAMEORIGIN` si ton propre site
|
|
280
280
|
s'auto-encadre. C'est un des trois en-têtes que security **ne ré-émet pas** : il doit valoir aussi
|
|
281
281
|
pour un HTML statique servi directement depuis `public/`.
|
|
282
282
|
|
|
@@ -287,9 +287,9 @@ pour un HTML statique servi directement depuis `public/`.
|
|
|
287
287
|
avec tes cookies.
|
|
288
288
|
|
|
289
289
|
Valeur unique reconnue : `nosniff`, posée depuis le cache `secContentTypeOptions`
|
|
290
|
-
(`http-kernel.ts
|
|
290
|
+
(`http-kernel.ts#HttpKernel.secContentTypeOptions`). C'est **l'en-tête qui justifie le mieux la couche transport** : le danger
|
|
291
291
|
vient précisément des fichiers servis hors pipeline applicatif — un banc live le prouve sur une 404
|
|
292
|
-
(`security-headers.test.ts
|
|
292
|
+
(`security-headers.test.ts`).
|
|
293
293
|
|
|
294
294
|
### `Strict-Transport-Security` — HTTPS, et rien d'autre
|
|
295
295
|
|
|
@@ -297,15 +297,15 @@ vient précisément des fichiers servis hors pipeline applicatif — un banc liv
|
|
|
297
297
|
installer un intercepteur.
|
|
298
298
|
|
|
299
299
|
La chaîne est assemblée au boot par `HttpKernel.computeSecurityHeaderCaches()`
|
|
300
|
-
(`http-kernel.ts
|
|
300
|
+
(`http-kernel.ts#HttpKernel.computeSecurityHeaderCaches`) : `max-age`, puis `includeSubDomains` et `preload` selon la config.
|
|
301
301
|
|
|
302
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
|
|
303
|
+
de serveur (`http-kernel.ts#HttpKernel.secHsts`). C'est conforme à la RFC 6797, qui veut qu'un HSTS reçu en clair
|
|
304
304
|
soit ignoré : l'émettre sur du HTTP simple ne ferait que polluer. Défaut : un an, sous-domaines
|
|
305
305
|
inclus.
|
|
306
306
|
|
|
307
307
|
> [!CAUTION]
|
|
308
|
-
> `preload: true` (`http/nodefony/config/config.ts
|
|
308
|
+
> `preload: true` (`http/nodefony/config/config.ts#strictTransportSecuritySchema`) inscrit ton domaine dans la liste
|
|
309
309
|
> pré-chargée des navigateurs. C'est un **engagement quasi irréversible** : tout sous-domaine
|
|
310
310
|
> incapable de servir en HTTPS devient inaccessible, et la sortie de liste prend des mois. À ne
|
|
311
311
|
> jamais activer « pour voir ».
|
|
@@ -315,7 +315,7 @@ inclus.
|
|
|
315
315
|
**La menace** : la fuite d'URL. Chemins parlants, identifiants de session dans une query, jetons de
|
|
316
316
|
réinitialisation — tout part chez le site suivant via le `Referer`.
|
|
317
317
|
|
|
318
|
-
Défaut Nodefony : `no-referrer` (`security/nodefony/config/config.ts
|
|
318
|
+
Défaut Nodefony : `no-referrer` (`security/nodefony/config/config.ts#headersSchema`), la valeur la plus stricte. La valeur est un
|
|
319
319
|
**enum W3C fermé** — huit valeurs validées au boot, donc pas de faute de frappe qui passerait en
|
|
320
320
|
silence (l'écriture libre `no-refferer` casserait la protection sans prévenir).
|
|
321
321
|
|
|
@@ -328,7 +328,7 @@ interne, origine seule vers l'extérieur, rien du tout vers du HTTP en clair.
|
|
|
328
328
|
`window.opener` et partage un groupe de contexte de navigation — surface d'attaque pour du
|
|
329
329
|
_tabnabbing_ et pour les canaux auxiliaires type Spectre.
|
|
330
330
|
|
|
331
|
-
`same-origin` (`securityHeaders.ts
|
|
331
|
+
`same-origin` (`securityHeaders.ts#SecurityHeaders.constructor`) rompt ce lien. C'est aussi, avec COEP, l'une des deux
|
|
332
332
|
conditions de l'**isolation cross-origin**, indispensable si tu veux `SharedArrayBuffer` ou des
|
|
333
333
|
timers haute résolution.
|
|
334
334
|
|
|
@@ -337,13 +337,13 @@ timers haute résolution.
|
|
|
337
337
|
**La menace** : ta page embarque des ressources tierces qui n'ont jamais donné leur accord, et les
|
|
338
338
|
place dans ton processus.
|
|
339
339
|
|
|
340
|
-
`require-corp` (`securityHeaders.ts
|
|
340
|
+
`require-corp` (`securityHeaders.ts#SecurityHeaders.constructor`) exige que **chaque** ressource tierce s'annonce comme
|
|
341
341
|
partageable (CORP ou CORS). C'est le complément de COOP pour l'isolation complète.
|
|
342
342
|
|
|
343
343
|
> [!WARNING]
|
|
344
344
|
> `coep: "require-corp"` **casse toutes les ressources tierces non conformes** — polices Google,
|
|
345
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
|
|
346
|
+
> volontairement exclu du banc de test (`security-headers.test.ts`). À activer seulement si tu
|
|
347
347
|
> as besoin de l'isolation cross-origin, et après audit de tes assets.
|
|
348
348
|
|
|
349
349
|
### `Cross-Origin-Resource-Policy` — mes assets ne s'incluent pas ailleurs
|
|
@@ -351,7 +351,7 @@ partageable (CORP ou CORS). C'est le complément de COOP pour l'isolation compl
|
|
|
351
351
|
**La menace** : symétrique du précédent. Un site tiers inclut tes images ou tes scripts pour les
|
|
352
352
|
mesurer, les mettre en cache, ou monter une attaque par inclusion.
|
|
353
353
|
|
|
354
|
-
`same-origin` (`securityHeaders.ts
|
|
354
|
+
`same-origin` (`securityHeaders.ts#SecurityHeaders.constructor`) interdit toute inclusion externe ; `same-site` autorise tes
|
|
355
355
|
propres sous-domaines ; `cross-origin` ouvre — c'est ce qu'il faut sur une CDN publique assumée.
|
|
356
356
|
|
|
357
357
|
### `Origin-Agent-Cluster` — un bac à sable par origine
|
|
@@ -359,15 +359,15 @@ propres sous-domaines ; `cross-origin` ouvre — c'est ce qu'il faut sur une CDN
|
|
|
359
359
|
**La menace** : plusieurs origines partageant heap et processus, donc des canaux auxiliaires
|
|
360
360
|
mesurables.
|
|
361
361
|
|
|
362
|
-
Nodefony l'émet comme un **booléen de champ structuré** RFC
|
|
363
|
-
(`securityHeaders.ts
|
|
362
|
+
Nodefony l'émet comme un **booléen de champ structuré** RFC 9651 : la valeur est littéralement `?1`
|
|
363
|
+
(`securityHeaders.ts#SecurityHeaders.constructor`). C'est une **demande**, pas une garantie — le navigateur décide.
|
|
364
364
|
|
|
365
365
|
### `Permissions-Policy` — coupe le micro par défaut
|
|
366
366
|
|
|
367
367
|
**La menace** : une iframe tierce (widget, publicité) demande la caméra, le micro ou la position, et
|
|
368
368
|
la boîte de dialogue s'affiche sous **ton** nom de domaine.
|
|
369
369
|
|
|
370
|
-
Valeur libre — le champ `permissionsPolicy` est recopié tel quel (`securityHeaders.ts
|
|
370
|
+
Valeur libre — le champ `permissionsPolicy` est recopié tel quel (`securityHeaders.ts#ISecurityHeadersOptions.permissionsPolicy`),
|
|
371
371
|
typiquement `camera=(), microphone=(), geolocation=()` : la
|
|
372
372
|
liste vide signifie « personne, pas même moi ». Absent par défaut car la liste des fonctionnalités
|
|
373
373
|
dépend entièrement de l'application.
|
|
@@ -379,7 +379,7 @@ config security est la première source de confusion sur ce sujet.
|
|
|
379
379
|
|
|
380
380
|
### Couche applicative — `use("@nodefony/security", { headers })`
|
|
381
381
|
|
|
382
|
-
Dérivé du schéma Zod `headersSchema` (`config.ts
|
|
382
|
+
Dérivé du schéma Zod `headersSchema` (`config.ts#headersSchema`).
|
|
383
383
|
|
|
384
384
|
<!-- prettier-ignore -->
|
|
385
385
|
| Option | Type | Défaut | Effet |
|
|
@@ -396,8 +396,8 @@ Dérivé du schéma Zod `headersSchema` (`config.ts:206`).
|
|
|
396
396
|
|
|
397
397
|
### Socle transport — `use("@nodefony/http", { securityHeaders })`
|
|
398
398
|
|
|
399
|
-
Dérivé de `securityHeadersSchema` (`http/nodefony/config/config.ts
|
|
400
|
-
**éditables à chaud** (`runtimeMutable`) : `HttpKernel.onConfigChanged()` (`http-kernel.ts
|
|
399
|
+
Dérivé de `securityHeadersSchema` (`http/nodefony/config/config.ts#securityHeadersSchema`). Ces trois réglages sont
|
|
400
|
+
**éditables à chaud** (`runtimeMutable`) : `HttpKernel.onConfigChanged()` (`http-kernel.ts#HttpKernel.onConfigChanged`)
|
|
401
401
|
recalcule les caches, donc la valeur suivante s'applique sans redémarrage.
|
|
402
402
|
|
|
403
403
|
| Option | Type | Défaut | Effet |
|
|
@@ -411,10 +411,10 @@ recalcule les caches, donc la valeur suivante s'applique sans redémarrage.
|
|
|
411
411
|
|
|
412
412
|
> [!WARNING]
|
|
413
413
|
> Les clés `hsts`, `hstsMaxAgeS`, `frameguard` et `noSniff` **existent** dans la config security
|
|
414
|
-
> (`config.ts
|
|
415
|
-
> applicative ne les lit pas (`securityHeaders.ts
|
|
414
|
+
> (`config.ts#headersSchema`) mais **ne pilotent rien** : la couche
|
|
415
|
+
> applicative ne les lit pas (`securityHeaders.ts#ISecurityHeadersOptions`), elles ne servent qu'à l'introspection
|
|
416
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
|
|
417
|
+
> **du module http**. Même remarque pour `hidePoweredBy` (`config.ts#headersSchema`) : Nodefony n'émet aucun
|
|
418
418
|
> `X-Powered-By`, l'option est un no-op documenté.
|
|
419
419
|
|
|
420
420
|
## 🏗️ Le CSP en détail — deux régimes, trois façons de l'étendre
|
|
@@ -422,11 +422,11 @@ recalcule les caches, donc la valeur suivante s'applique sans redémarrage.
|
|
|
422
422
|
### Régime 1 — CSP statique (0 allocation par requête)
|
|
423
423
|
|
|
424
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
|
|
425
|
+
quelle dans la table gelée du boot (`securityHeaders.ts#SecurityHeaders.constructor`). Par requête : une lecture, un
|
|
426
426
|
`setHeader`. Rien d'autre.
|
|
427
427
|
|
|
428
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
|
|
429
|
+
token résiduel `'nonce-{{nonce}}'` est **purgé** (`securityHeaders.ts#SecurityHeaders.constructor`). Sans ça, tu servirais un
|
|
430
430
|
CSP contenant un nonce littéral jamais émis — donc un `script-src` qui bloque **tout**, y compris tes
|
|
431
431
|
propres scripts. Le code refuse de produire un CSP cassé.
|
|
432
432
|
|
|
@@ -437,22 +437,22 @@ attaquant ne peut pas deviner la valeur : il est refusé même s'il est syntaxiq
|
|
|
437
437
|
|
|
438
438
|
Le chemin complet, sans surprise :
|
|
439
439
|
|
|
440
|
-
1. **Au boot**, la chaîne CSP est **pré-découpée** autour de `{{nonce}}` (`securityHeaders.ts
|
|
440
|
+
1. **Au boot**, la chaîne CSP est **pré-découpée** autour de `{{nonce}}` (`securityHeaders.ts#SecurityHeaders.constructor`).
|
|
441
441
|
Aucun parsing ni regex n'aura lieu pendant une requête.
|
|
442
|
-
2. **Par requête**, `Firewall.applySecurityHeaders()` (`firewall.ts
|
|
443
|
-
ce qui **génère** le jeton à cet instant (`Context.ts
|
|
444
|
-
`SecurityHeaders.cspFor()` (`securityHeaders.ts
|
|
442
|
+
2. **Par requête**, `Firewall.applySecurityHeaders()` (`firewall.ts#Firewall.applySecurityHeaders`) lit `context.cspNonce` —
|
|
443
|
+
ce qui **génère** le jeton à cet instant (`Context.ts#Context.#cspNonce`) — puis appelle
|
|
444
|
+
`SecurityHeaders.cspFor()` (`securityHeaders.ts#SecurityHeaders.cspFor`) : un seul `join`.
|
|
445
445
|
3. **Dans la vue**, le contrôleur relit `context.cspNonce`, qui est **mémoïsé** : l'en-tête et le
|
|
446
446
|
`<script nonce="…">` portent forcément la même valeur. C'est le motif employé par le contrôleur
|
|
447
|
-
de Studio (`StudioController.ts
|
|
447
|
+
de Studio (`StudioController.ts#StudioController.renderStudio`).
|
|
448
448
|
|
|
449
|
-
`SecurityHeaders.hasNonce` (`securityHeaders.ts
|
|
449
|
+
`SecurityHeaders.hasNonce` (`securityHeaders.ts#SecurityHeaders.hasNonce`) est le drapeau qui décide du régime : à `false`,
|
|
450
450
|
pas une seule opération crypto. Il n'y a **aucun setter** pour `cspNonce` : un jeton serveur doit
|
|
451
451
|
rester imprévisible, jamais pilotable par le client — contrairement au `requestId`, qui, lui, accepte
|
|
452
452
|
une corrélation entrante.
|
|
453
453
|
|
|
454
454
|
**Placement dans le pipeline** : `applySecurityHeaders` est appelé **après le resolve** et **avant**
|
|
455
|
-
le repli statique et le `writeHead` (`http-kernel.ts
|
|
455
|
+
le repli statique et le `writeHead` (`http-kernel.ts#HttpKernel.runHttpPipeline`). Cet ordre n'est pas cosmétique : il
|
|
456
456
|
faut que le routeur ait posé les directives `@Csp` de la route pour pouvoir les fusionner, et il faut
|
|
457
457
|
être avant l'écriture des en-têtes pour pouvoir en poser.
|
|
458
458
|
|
|
@@ -464,21 +464,21 @@ faut que le routeur ait posé les directives `@Csp` de la route pour pouvoir les
|
|
|
464
464
|
| Module | `Firewall.registerCspOrigins()` | besoin **permanent** d'un module (ex. Vite) | à l'enregistrement |
|
|
465
465
|
| Route / contrôleur | `@Csp({ … })` | besoin **ponctuel** d'une réponse (iframe, CDN) | par requête décorée |
|
|
466
466
|
|
|
467
|
-
Les trois convergent vers `mergeCspFragments()` (`csp.ts
|
|
467
|
+
Les trois convergent vers `mergeCspFragments()` (`csp.ts#mergeCspFragments`), et c'est un **merge structuré**, pas
|
|
468
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
|
|
469
|
+
première occurrence (W3C CSP Level 3 §3, rationnel documenté `csp.ts#CspFragment`). Concaténer
|
|
470
470
|
`"script-src 'self'"` et `"script-src https://cdn"` produirait un en-tête où la seconde est purement
|
|
471
471
|
et simplement jetée — une extension silencieusement sans effet, le pire des deux mondes.
|
|
472
472
|
|
|
473
473
|
Le merge fusionne donc les sources **dans une seule directive**, dédoublonnées, base d'abord ; une
|
|
474
474
|
directive absente est ajoutée en fin. La fonction est **pure et déterministe** (`parseCsp()`
|
|
475
|
-
`csp.ts
|
|
475
|
+
`csp.ts#parseCsp` → `serializeCsp()` `csp.ts#serializeCsp`), ce qui rend l'en-tête stable d'une requête à l'autre et
|
|
476
476
|
les tests fiables.
|
|
477
477
|
|
|
478
478
|
**Coût** : le merge d'un module est payé **une fois**, au (dés)enregistrement
|
|
479
|
-
(`Firewall.#rebuildSecurityHeaders()`, `firewall.ts
|
|
479
|
+
(`Firewall.#rebuildSecurityHeaders()`, `firewall.ts#Firewall.#rebuildSecurityHeaders`), jamais par requête. Le merge d'une route
|
|
480
480
|
`@Csp` est payé **uniquement sur les routes décorées** (`SecurityHeaders.cspForExtra()`,
|
|
481
|
-
`securityHeaders.ts
|
|
481
|
+
`securityHeaders.ts#SecurityHeaders.cspForExtra`) ; le cas courant reste le simple `join`.
|
|
482
482
|
|
|
483
483
|
## 🧩 Extension — déclarer un fragment CSP depuis son module
|
|
484
484
|
|
|
@@ -502,44 +502,44 @@ Trois propriétés à retenir :
|
|
|
502
502
|
|
|
503
503
|
- **Aucun couplage** : la résolution par nom de service évite un cycle de dépendances, et
|
|
504
504
|
`registerCspOrigins` est optionnel — un module fonctionne dans une app **sans** security.
|
|
505
|
-
- **Réversible** : `Firewall.unregisterCspOrigins()` (`firewall.ts
|
|
505
|
+
- **Réversible** : `Firewall.unregisterCspOrigins()` (`firewall.ts#Firewall.unregisterCspOrigins`) retire le fragment et
|
|
506
506
|
reconstruit le CSP de base. C'est ce que fait `@nodefony/frontend` à l'arrêt du serveur Vite.
|
|
507
507
|
- **Idempotent** : la reconstruction repart **toujours** du `headers.csp` d'origine
|
|
508
|
-
(`firewall.ts
|
|
508
|
+
(`firewall.ts#Firewall.#rebuildSecurityHeaders`), jamais d'un CSP déjà fusionné — pas d'accumulation entre deux
|
|
509
509
|
enregistrements.
|
|
510
510
|
|
|
511
511
|
L'exemple de référence vit dans le framework : en développement, `@nodefony/frontend` déclare les
|
|
512
512
|
origines du serveur Vite et `'unsafe-eval'` (exigé par le Fast Refresh de React) via
|
|
513
|
-
`FrontendService.#viteCspFragment()` (`FrontendService.ts
|
|
513
|
+
`FrontendService.#viteCspFragment()` (`FrontendService.ts#FrontendService.#viteCspFragment`) — ce qui explique qu'un CSP observé
|
|
514
514
|
en dev soit plus large qu'en production, où ce fragment n'existe pas.
|
|
515
515
|
|
|
516
516
|
## 📜 Normes appliquées
|
|
517
517
|
|
|
518
|
-
| Domaine | Norme | Ancrage dans le code
|
|
519
|
-
| ------------------------------------ | -------------------------------- |
|
|
520
|
-
| Politique de sécurité du contenu | W3C CSP Level 3 | `mergeCspFragments()` (`csp.ts
|
|
521
|
-
| Nonce CSP (unicité, imprévisibilité) | W3C CSP Level 3 §6.7.4 | `Context.cspNonce` — 128 bits CSPRNG (`Context.ts
|
|
522
|
-
| HSTS | RFC 6797 | posé sur TLS uniquement (`http-kernel.ts
|
|
523
|
-
| Champ structuré booléen | RFC
|
|
524
|
-
| Referrer-Policy | W3C Referrer Policy (enum fermé) | 8 valeurs validées au boot (`config.ts
|
|
525
|
-
| Isolation cross-origin | WHATWG HTML (COOP/COEP/CORP) | `securityHeaders.ts
|
|
526
|
-
| Anti-MIME-sniffing | WHATWG Fetch (`nosniff`) | `secContentTypeOptions` (`http-kernel.ts
|
|
527
|
-
| Durcissement en-têtes | OWASP Secure Headers | `computeSecurityHeaderCaches()` (`http-kernel.ts
|
|
518
|
+
| Domaine | Norme | Ancrage dans le code |
|
|
519
|
+
| ------------------------------------ | -------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
520
|
+
| Politique de sécurité du contenu | W3C CSP Level 3 | `mergeCspFragments()` (`csp.ts#mergeCspFragments`), directive non dupliquée |
|
|
521
|
+
| Nonce CSP (unicité, imprévisibilité) | W3C CSP Level 3 §6.7.4 | `Context.cspNonce` — 128 bits CSPRNG (`Context.ts#Context.#cspNonce`) |
|
|
522
|
+
| HSTS | RFC 6797 | posé sur TLS uniquement (`http-kernel.ts#HttpKernel.getErrorRenderer`) |
|
|
523
|
+
| Champ structuré booléen | RFC 9651 | `Origin-Agent-Cluster: ?1` (`securityHeaders.ts#SecurityHeaders.constructor`) |
|
|
524
|
+
| Referrer-Policy | W3C Referrer Policy (enum fermé) | 8 valeurs validées au boot (`config.ts#headersSchema`) |
|
|
525
|
+
| Isolation cross-origin | WHATWG HTML (COOP/COEP/CORP) | `securityHeaders.ts#SecurityHeaders.constructor` |
|
|
526
|
+
| Anti-MIME-sniffing | WHATWG Fetch (`nosniff`) | `secContentTypeOptions` (`http-kernel.ts#HttpKernel.secContentTypeOptions`) |
|
|
527
|
+
| Durcissement en-têtes | OWASP Secure Headers | `computeSecurityHeaderCaches()` (`http-kernel.ts#HttpKernel.computeSecurityHeaderCaches`) |
|
|
528
528
|
|
|
529
529
|
## ⚡ Performance & mémoire
|
|
530
530
|
|
|
531
531
|
Le coût est concentré au boot, par construction :
|
|
532
532
|
|
|
533
|
-
- **En-têtes constants** : une seule table, gelée (`securityHeaders.ts
|
|
533
|
+
- **En-têtes constants** : une seule table, gelée (`securityHeaders.ts#SecurityHeaders.constructor`). Par requête, une boucle
|
|
534
534
|
`for…in` sur un objet de 1 à 6 entrées et autant de `setHeader`. Aucune allocation.
|
|
535
535
|
- **CSP statique** : rien de plus — la chaîne est dans la table.
|
|
536
536
|
- **CSP à nonce** : un tirage dans le pool plus une concaténation `préfixe + nonce + suffixe` 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
|
|
538
|
-
court-circuite entièrement ce chemin sinon. La paresse de `Context.cspNonce` (`Context.ts
|
|
537
|
+
n'existe **que** si le CSP porte un placeholder : `hasNonce` (`securityHeaders.ts#SecurityHeaders.hasNonce`)
|
|
538
|
+
court-circuite entièrement ce chemin sinon. La paresse de `Context.cspNonce` (`Context.ts#Context.#cspNonce`)
|
|
539
539
|
protège en plus les chemins internes qui n'atteignent jamais le firewall.
|
|
540
540
|
- **Merge CSP** : jamais dans le chemin chaud. Le fragment d'un module est fusionné à
|
|
541
|
-
l'enregistrement (`firewall.ts
|
|
542
|
-
- **Socle transport** : trois `setHeader` sur des chaînes précalculées (`http-kernel.ts
|
|
541
|
+
l'enregistrement (`firewall.ts#Firewall.currentRevalidator`) ; 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#HttpKernel.dispatchHttpRequest`), avec
|
|
543
543
|
un test `!== null` qui annule le coût des en-têtes désactivés.
|
|
544
544
|
|
|
545
545
|
Le module n'attache aucun écouteur d'événement et ne conserve aucun état par requête : il n'entre pas
|
|
@@ -548,9 +548,9 @@ dans le périmètre du gate mémoire, qu'il ne peut structurellement pas dégrad
|
|
|
548
548
|
## 📡 Observabilité — Studio
|
|
549
549
|
|
|
550
550
|
L'écran **Firewall** de Studio affiche la section « En-têtes de sécurité » — pilotée par
|
|
551
|
-
`headers.enabled` (`FirewallDefenses.tsx
|
|
551
|
+
`headers.enabled` (`FirewallDefenses.tsx#FirewallDefenses`) — avec le CSP effectif, l'état du nonce par requête, la
|
|
552
552
|
Referrer-Policy et les valeurs d'isolation. Les données
|
|
553
|
-
viennent de `Firewall.describe()` (`firewall.ts
|
|
553
|
+
viennent de `Firewall.describe()` (`firewall.ts#Firewall.describe`), qui projette la config **sans aucun secret**,
|
|
554
554
|
exposée par `GET /nodefony/security/api/firewall`.
|
|
555
555
|
|
|
556
556
|
L'onglet **Configuration** de Studio rend les mêmes options depuis le schéma Zod — chaque champ y
|
|
@@ -563,17 +563,17 @@ porte sa description, ce qui en fait la référence toujours à jour des défaut
|
|
|
563
563
|
|
|
564
564
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
565
565
|
|
|
566
|
-
| Symptôme | Cause dans le code
|
|
567
|
-
| -------------------------------------------------------- |
|
|
568
|
-
| `frameguard: "sameorigin"` en config security sans effet | Clé **inerte** — non lue par la couche applicative (`securityHeaders.ts
|
|
569
|
-
| `<script>` inline bloqué | Le template ne reprend pas le nonce
|
|
570
|
-
| CSP contenant `'nonce-{{nonce}}'` littéral | `cspNonces` désactivé, placeholder laissé
|
|
571
|
-
| Une extension `script-src` de module sans effet | Second en-tête / directive dupliquée (ignorée, W3C CSP3 §3)
|
|
572
|
-
| Assets tiers cassés après activation de l'isolation | `coep: "require-corp"` exige CORP/CORS sur **chaque** ressource
|
|
573
|
-
| HSTS absent en développement | Posé sur TLS uniquement (`http-kernel.ts
|
|
574
|
-
| Le CSP est plus large en dev qu'en prod | `@nodefony/frontend` déclare les origines Vite (`FrontendService.ts
|
|
575
|
-
| Aucun en-tête applicatif | Module security absent ou `headers.enabled: false`
|
|
576
|
-
| Domaine injoignable après activation de `preload` | Inscription à la liste pré-chargée, sortie très lente
|
|
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#ISecurityHeadersOptions`) | 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#ISecurityHeadersOptions.cspNonces`) ; 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#HttpKernel.getErrorRenderer`) | 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#FrontendService.#buildEntries`) | 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
577
|
|
|
578
578
|
## 🧪 Tests & couverture
|
|
579
579
|
|
|
@@ -587,12 +587,12 @@ depuis vitest :
|
|
|
587
587
|
- **Intégration sur serveur réel** (`@nodefony/http`, port 5151) — `security-headers.test` vérifie les
|
|
588
588
|
deux couches sur une **vraie** réponse. Quatre invariants y sont prouvés :
|
|
589
589
|
- le socle transport survit à une 404, avec `x-content-type-options` sur une route inexistante
|
|
590
|
-
(`security-headers.test.ts
|
|
590
|
+
(`security-headers.test.ts`) ;
|
|
591
591
|
- le CSP applicatif complète cette réponse avec `content-security-policy`
|
|
592
|
-
(`security-headers.test.ts
|
|
592
|
+
(`security-headers.test.ts`) ;
|
|
593
593
|
- une route décorée voit sa directive `frame-src` fusionnée, sans dupliquer `img-src`
|
|
594
|
-
(`security-headers.test.ts
|
|
595
|
-
- deux requêtes concurrentes reçoivent deux nonces différents (`security-headers.test.ts
|
|
594
|
+
(`security-headers.test.ts`) ;
|
|
595
|
+
- deux requêtes concurrentes reçoivent deux nonces différents (`security-headers.test.ts`).
|
|
596
596
|
|
|
597
597
|
`headers.test` et `security.test` couvrent le reste du contrat d'en-têtes.
|
|
598
598
|
|
package/docs/index.md
CHANGED
|
@@ -59,9 +59,11 @@ Quatre parcours selon ce que tu viens faire. L'ordre compte : chaque étape supp
|
|
|
59
59
|
**J'ouvre un login à des humains** — navigateur, comptes, second facteur.
|
|
60
60
|
|
|
61
61
|
1. [Authenticators](authenticators.md) — la session BFF, et pourquoi le login est **déjà fourni**.
|
|
62
|
-
2. [
|
|
63
|
-
3. [
|
|
64
|
-
4. [
|
|
62
|
+
2. [Page de connexion](login-page.md) — la page servie à `/login`, et comment l'habiller à ton nom.
|
|
63
|
+
3. [OAuth2](oauth2.md) — « se connecter avec GitHub/Google » et le Shadow User.
|
|
64
|
+
4. [Keycloak](keycloak.md) — brancher l'annuaire de l'entreprise, du realm au premier login.
|
|
65
|
+
5. [WebAuthn / passkeys](webauthn.md) — se connecter sans mot de passe, résistant au phishing.
|
|
66
|
+
6. [TOTP](totp.md) — le second facteur classique, et l'élévation de privilège (step-up).
|
|
65
67
|
|
|
66
68
|
**J'audite avant une mise en production** — la passe qu'on regrette de ne pas avoir faite.
|
|
67
69
|
|
|
@@ -86,7 +88,9 @@ Le tableau pour choisir en cinq secondes ; les cards en dessous pour le détail.
|
|
|
86
88
|
| [CSRF](csrf.md) | requête authentifiée forgée par un site tiers | tu sers un front avec cookie de session |
|
|
87
89
|
| [CORS](cors.md) | lecture cross-origine de tes réponses | ton front est sur un autre domaine |
|
|
88
90
|
| [En-têtes](headers.md) | CSP, HSTS, COOP/COEP, Referrer-Policy | tu sers du HTML à un navigateur |
|
|
91
|
+
| [Page de connexion](login-page.md) | la page `/login` servie, habillable | des humains se connectent à ton application |
|
|
89
92
|
| [OAuth2](oauth2.md) | login social + provisionnement d'identité | « se connecter avec … » |
|
|
93
|
+
| [Keycloak](keycloak.md) | connexion par l'annuaire auto-hébergé | tes comptes vivent dans un realm Keycloak |
|
|
90
94
|
| [WebAuthn](webauthn.md) | passkeys, connexion résistante au phishing | tu veux supprimer les mots de passe |
|
|
91
95
|
| [TOTP](totp.md) | second facteur temporel + step-up | 2FA, ou re-preuve avant une action sensible |
|
|
92
96
|
| [Webhooks](webhooks.md) | notifier un tiers, signé et sans SSRF | un système externe doit réagir à tes événements |
|
|
@@ -103,6 +107,9 @@ Le tableau pour choisir en cinq secondes ; les cards en dessous pour le détail.
|
|
|
103
107
|
{ "icon": "⚖️", "title": "authorization", "href": "authorization.md",
|
|
104
108
|
"desc": "Décider des droits : rôles hiérarchisés, scopes, et voters métier qui portent le vrai pouvoir applicatif. Le jury vote, la stratégie tranche.",
|
|
105
109
|
"meta": "juste après les authenticators — authentifier sans autoriser ne protège rien" },
|
|
110
|
+
{ "icon": "🚪", "title": "page de connexion", "href": "login-page.md",
|
|
111
|
+
"desc": "La page servie à /login : identifiant, mot de passe, second facteur, boutons des fournisseurs, thèmes clair et sombre. Textes, panneau et couleurs se règlent par la configuration et une feuille de style — sans copier son balisage.",
|
|
112
|
+
"meta": "le piège : une couleur qui dépend du thème se redéfinit pour les DEUX thèmes" },
|
|
106
113
|
{ "icon": "🎫", "title": "tokens", "href": "tokens.md",
|
|
107
114
|
"desc": "L'identité matérialisée : émission, keystore, rotation, révocation — et le choix structurant du framework, session opaque côté serveur pour le web, JWT pour les API.",
|
|
108
115
|
"meta": "et pourquoi ce n'est pas « full stateless »" },
|
|
@@ -126,7 +133,7 @@ flowchart LR
|
|
|
126
133
|
```
|
|
127
134
|
|
|
128
135
|
Le module consomme `@nodefony/user` (jamais l'inverse) et n'importe `@nodefony/http` / `framework`
|
|
129
|
-
qu'en **type-only**, à une exception près : `schemaMismatchOf` (`webhooks.ts
|
|
136
|
+
qu'en **type-only**, à une exception près : `schemaMismatchOf` (`webhooks.ts`), importé de
|
|
130
137
|
`@nodefony/http` pour reconnaître une base au schéma en retard avec la même règle que le cœur HTTP,
|
|
131
138
|
plutôt qu'avec une copie.
|
|
132
139
|
|
|
@@ -154,14 +161,15 @@ sans ouvrir de port (profil console) :
|
|
|
154
161
|
| `nodefony security:user:password <id>` | change le mot de passe — **et révoque sessions et jetons** |
|
|
155
162
|
| `nodefony security:user:delete <id>` | supprime, après confirmation ; refuse le dernier administrateur |
|
|
156
163
|
| `nodefony security:secrets [--write]` | engendre les clés attendues et guide leur câblage |
|
|
157
|
-
| `nodefony security:token` | émet un jeton d'accès pour la porte MCP |
|
|
164
|
+
| `nodefony security:mcp:token` | émet un jeton d'accès pour la porte MCP |
|
|
165
|
+
| `nodefony security:keycloak:realm [--write\|--check]` | écrit le realm Keycloak déduit de la configuration ([Keycloak](keycloak.md)) |
|
|
158
166
|
|
|
159
167
|
En terminal, le mot de passe est **demandé masqué** et confirmé ; pour un script, `--password <pwd>`
|
|
160
168
|
l'accepte en argument — au prix de l'historique du shell, que la commande rappelle.
|
|
161
169
|
|
|
162
170
|
Deux garde-fous qui se constatent plutôt qu'ils ne se supposent : le **dernier administrateur actif
|
|
163
|
-
ne se supprime pas** (`security-user-delete.ts
|
|
164
|
-
accès en cours** par la même cascade que la suppression (`userRevocationCascade.ts
|
|
171
|
+
ne se supprime pas** (`security-user-delete.ts#SecurityUserDelete.generate`), et un changement de mot de passe **éjecte les
|
|
172
|
+
accès en cours** par la même cascade que la suppression (`userRevocationCascade.ts#KernelListenerLike`) — on change
|
|
165
173
|
un mot de passe parce qu'il est perdu ou compromis.
|
|
166
174
|
|
|
167
175
|
## ⚙️ Configuration
|
|
@@ -176,7 +184,7 @@ au boot. Blocs : `areas` (zones + authenticators), `cors`, `csrf`, `headers`, `r
|
|
|
176
184
|
<!-- prettier-ignore -->
|
|
177
185
|
| Domaine | Normes |
|
|
178
186
|
| --- | --- |
|
|
179
|
-
| Auth / challenge (401) | RFC
|
|
187
|
+
| Auth / challenge (401) | RFC 9110 §11 |
|
|
180
188
|
| JWT | RFC 7519, 8725 (BCP) |
|
|
181
189
|
| OAuth 2 | RFC 9700 (BCP), 8707, 8693, 9449 (DPoP) |
|
|
182
190
|
| Passkeys | W3C WebAuthn L3, CTAP2 |
|