fix(security): consolidation & securite phase 1 (#84, BUG-021 a BUG-034)
CI / lint (push) Successful in 1m20s
CI / security (push) Successful in 47s
CI / test (push) Successful in 2m21s
CI / build (push) Successful in 43s
CI / e2e (push) Successful in 10m48s

- sanitizer XSS serveur (markdown + page de partage) [BUG-021/022]
- rate-limit/lockout MFA [BUG-023]
- isolation vaults par segments [BUG-024]
- caps regex ReDoS [BUG-025]
- SSRF webhooks + secrets externalises [BUG-026]
- rotation/revocation des jetons [BUG-027]
- politique de mot de passe + invalidation sessions [BUG-028]
- verrous users.json [BUG-029]
- IP reelle dans les audits [BUG-030]
- rate-limit par compte [BUG-031]
- symlinks hors vault ignores [BUG-032]
- recherche simple via inverted index [BUG-033]
- token en memoire + cookie HttpOnly, CSP durcie [BUG-034]

Tests: pytest 961 passed / 6 skipped, ruff 0, mypy 0, frontend vert.
This commit is contained in:
2026-09-13 10:51:42 -04:00
parent d47fcbf2be
commit 162a5b4acc
29 changed files with 1690 additions and 281 deletions
+18 -6
View File
@@ -85,12 +85,18 @@ const AuthManager = {
TOKEN_EXPIRY_KEY: "obsigate_token_expiry",
USER_KEY: "obsigate_user",
_authEnabled: false,
// BUG-034: the access token is kept in memory only. The server also sets it
// as an HttpOnly cookie, so a page reload re-authenticates via /api/auth/refresh
// without ever exposing the token to JavaScript-readable storage.
_accessToken: null,
// ── Token storage (sessionStorage) ─────────────────────────────
// ── Token storage (in-memory + HttpOnly cookie) ────────────────
saveToken(tokenData) {
const expiresAt = Date.now() + tokenData.expires_in * 1000;
sessionStorage.setItem(this.ACCESS_TOKEN_KEY, tokenData.access_token);
this._accessToken = tokenData.access_token;
// Clear any token persisted by an older build (XSS-readable).
try { sessionStorage.removeItem(this.ACCESS_TOKEN_KEY); } catch (e) { /* ignore */ }
sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiresAt.toString());
if (tokenData.user) {
sessionStorage.setItem(this.USER_KEY, JSON.stringify(tokenData.user));
@@ -98,7 +104,11 @@ const AuthManager = {
},
getToken() {
return sessionStorage.getItem(this.ACCESS_TOKEN_KEY);
return this._accessToken;
},
hasSession() {
return !!this._accessToken || !!sessionStorage.getItem(this.TOKEN_EXPIRY_KEY);
},
getUser() {
@@ -114,7 +124,8 @@ const AuthManager = {
},
clearSession() {
sessionStorage.removeItem(this.ACCESS_TOKEN_KEY);
this._accessToken = null;
try { sessionStorage.removeItem(this.ACCESS_TOKEN_KEY); } catch (e) { /* ignore */ }
sessionStorage.removeItem(this.TOKEN_EXPIRY_KEY);
sessionStorage.removeItem(this.USER_KEY);
},
@@ -281,7 +292,8 @@ const AuthManager = {
}
const data = await response.json();
const expiry = Date.now() + data.expires_in * 1000;
sessionStorage.setItem(this.ACCESS_TOKEN_KEY, data.access_token);
this._accessToken = data.access_token;
try { sessionStorage.removeItem(this.ACCESS_TOKEN_KEY); } catch (e) { /* ignore */ }
sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiry.toString());
return data.access_token;
},
@@ -359,7 +371,7 @@ const AuthManager = {
}
// Auth enabled — check for existing session
if (this.getToken() && !this.isTokenExpired()) {
if (this.hasSession() && !this.isTokenExpired()) {
this.showApp();
return true;
}