Merge branch 'main' into celery-2-dramatiq

This commit is contained in:
Marc 'risson' Schmitt
2025-03-28 17:35:05 +01:00
27 changed files with 236 additions and 70 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.2.2
current_version = 2025.2.3
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@ -17,6 +17,8 @@ optional_value = final
[bumpversion:file:pyproject.toml]
[bumpversion:file:uv.lock]
[bumpversion:file:package.json]
[bumpversion:file:docker-compose.yml]

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2025.2.2"
__version__ = "2025.2.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,13 +1,14 @@
"""User API Views"""
from datetime import timedelta
from importlib import import_module
from json import loads
from typing import Any
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.contrib.sessions.backends.base import SessionBase
from django.db.models.functions import ExtractHour
from django.db.transaction import atomic
from django.db.utils import IntegrityError
@ -91,6 +92,7 @@ from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
class UserGroupSerializer(ModelSerializer):
@ -373,7 +375,7 @@ class UsersFilter(FilterSet):
method="filter_attributes",
)
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
uuid = UUIDFilter(field_name="uuid")
path = CharFilter(field_name="path")
@ -391,6 +393,11 @@ class UsersFilter(FilterSet):
queryset=Group.objects.all().order_by("name"),
)
def filter_is_superuser(self, queryset, name, value):
if value:
return queryset.filter(ak_groups__is_superuser=True).distinct()
return queryset.exclude(ak_groups__is_superuser=True).distinct()
def filter_attributes(self, queryset, name, value):
"""Filter attributes by query args"""
try:
@ -769,7 +776,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not instance.is_active:
sessions = AuthenticatedSession.objects.filter(user=instance)
session_ids = sessions.values_list("session_key", flat=True)
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
for session in session_ids:
SessionStore(session).delete()
sessions.delete()
LOGGER.debug("Deleted user's sessions", user=instance.username)
return response

View File

@ -1,7 +1,10 @@
"""authentik core signals"""
from importlib import import_module
from django.conf import settings
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.contrib.sessions.backends.base import SessionBase
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
@ -25,6 +28,7 @@ password_changed = Signal()
login_failed = Signal()
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
@receiver(post_save, sender=Application)
@ -60,8 +64,7 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
@receiver(pre_delete, sender=AuthenticatedSession)
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted"""
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)
SessionStore(instance.session_key).delete()
@receiver(pre_save)

View File

@ -36,6 +36,7 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_values
@ -48,6 +49,7 @@ LOGGER = get_logger()
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
SESSION_KEY_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context"
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
@ -208,6 +210,8 @@ class SourceFlowManager:
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
flow_context.update(
{
# Since we authenticate the user by their token, they have no backend set
@ -261,6 +265,7 @@ class SourceFlowManager:
plan.append_stage(stage)
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
plan.append_stage(stage)
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
return plan.to_redirect(self.request, flow)
def handle_auth(

View File

@ -1,6 +1,7 @@
"""Test Users API"""
from datetime import datetime
from json import loads
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
@ -15,7 +16,11 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.core.tests.utils import (
create_test_admin_user,
create_test_brand,
create_test_flow,
)
from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.email.models import EmailStage
@ -41,6 +46,32 @@ class TestUsersAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
def test_filter_is_superuser(self):
"""Test API filtering by superuser status"""
self.client.force_login(self.admin)
# Test superuser
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": True,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1)
self.assertEqual(body["results"][0]["username"], self.admin.username)
# Test non-superuser
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": False,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1, body)
self.assertEqual(body["results"][0]["username"], self.user.username)
def test_list_with_groups(self):
"""Test listing with groups"""
self.client.force_login(self.admin)

View File

@ -11,13 +11,14 @@ from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Source, User
from authentik.core.sources.flow_manager import (
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
SESSION_KEY_SOURCE_FLOW_CONTEXT,
SESSION_KEY_SOURCE_FLOW_STAGES,
)
from authentik.core.types import UILoginButton
from authentik.enterprise.stages.source.models import SourceStage
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.models import FlowToken, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
from authentik.flows.planner import PLAN_CONTEXT_IS_REDIRECTED, PLAN_CONTEXT_IS_RESTORED
from authentik.flows.stage import ChallengeStageView, StageView
from authentik.lib.utils.time import timedelta_from_string
@ -53,6 +54,9 @@ class SourceStageView(ChallengeStageView):
resume_token = self.create_flow_token()
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
self.request.session[SESSION_KEY_SOURCE_FLOW_CONTEXT] = {
PLAN_CONTEXT_IS_REDIRECTED: self.executor.flow,
}
return self.login_button.challenge
def create_flow_token(self) -> FlowToken:

View File

@ -33,6 +33,7 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
@ -73,6 +74,8 @@ class InitiateView(View):
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
kwargs.update(
{
PLAN_CONTEXT_SSO: True,

View File

@ -142,35 +142,38 @@ class IdentificationChallengeResponse(ChallengeResponse):
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
# Password check
if current_stage.password_stage:
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
# Captcha check
if captcha_stage := current_stage.captcha_stage:
captcha_token = attrs.get("captcha_token", None)
if not captcha_token:
self.stage.logger.warning("Token not set for captcha attempt")
verify_captcha_token(captcha_stage, captcha_token, client_ip)
# Password check
if not current_stage.password_stage:
# No password stage select, don't validate the password
return attrs
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
return attrs

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2025.2.2 Blueprint schema",
"title": "authentik 2025.2.3 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
restart: unless-stopped
command: server
environment:
@ -54,7 +54,7 @@ services:
redis:
condition: service_healthy
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
restart: unless-stopped
command: worker
environment:

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2025.2.2"
const VERSION = "2025.2.3"

View File

@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2025.2.2
Default: 2025.2.3
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2025.2.2",
"version": "2025.2.3",
"private": true
}

View File

@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2025.2.2"
version = "2025.2.3"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.12.*"

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2025.2.2
version: 2025.2.3
description: Making authentication simple.
contact:
email: hello@goauthentik.io

2
uv.lock generated
View File

@ -162,7 +162,7 @@ wheels = [
[[package]]
name = "authentik"
version = "2025.2.2"
version = "2025.2.3"
source = { editable = "." }
dependencies = [
{ name = "argon2-cffi" },

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2025.2.2";
export const VERSION = "2025.2.3";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -72,7 +72,9 @@ export class BaseStage<
}
return this.host?.submit(object as unknown as Tout).then((successful) => {
if (successful) {
this.cleanup();
this.onSubmitSuccess();
} else {
this.onSubmitFailure();
}
return successful;
});
@ -124,7 +126,11 @@ export class BaseStage<
`;
}
cleanup(): void {
onSubmitSuccess(): void {
// Method that can be overridden by stages
return;
}
onSubmitFailure(): void {
// Method that can be overridden by stages
return;
}

View File

@ -9,7 +9,7 @@ import { randomId } from "@goauthentik/elements/utils/randomId";
import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { P, match } from "ts-pattern";
import type { TurnstileObject } from "turnstile-types";
import type * as _ from "turnstile-types";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
@ -24,10 +24,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
interface TurnstileWindow extends Window {
turnstile: TurnstileObject;
}
type TokenHandler = (token: string) => void;
type Dims = { height: number };
@ -52,6 +48,8 @@ type CaptchaHandler = {
name: string;
interactive: () => Promise<unknown>;
execute: () => Promise<unknown>;
refreshInteractive: () => Promise<unknown>;
refresh: () => Promise<unknown>;
};
// A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces
@ -119,6 +117,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
this.host.submit({ component: "ak-stage-captcha", token });
};
@property({ attribute: false })
refreshedAt = new Date();
@state()
activeHandler?: CaptchaHandler = undefined;
@state()
error?: string;
@ -127,16 +131,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
name: "grecaptcha",
interactive: this.renderGReCaptchaFrame,
execute: this.executeGReCaptcha,
refreshInteractive: this.refreshGReCaptchaFrame,
refresh: this.refreshGReCaptcha,
},
{
name: "hcaptcha",
interactive: this.renderHCaptchaFrame,
execute: this.executeHCaptcha,
refreshInteractive: this.refreshHCaptchaFrame,
refresh: this.refreshHCaptcha,
},
{
name: "turnstile",
interactive: this.renderTurnstileFrame,
execute: this.executeTurnstile,
refreshInteractive: this.refreshTurnstileFrame,
refresh: this.refreshTurnstile,
},
];
@ -230,6 +240,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
});
}
async refreshGReCaptchaFrame() {
(this.captchaFrame.contentWindow as typeof window)?.grecaptcha.reset();
}
async refreshGReCaptcha() {
window.grecaptcha.reset();
window.grecaptcha.execute();
}
async renderHCaptchaFrame() {
this.renderFrame(
html`<div
@ -251,6 +270,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
);
}
async refreshHCaptchaFrame() {
(this.captchaFrame.contentWindow as typeof window)?.hcaptcha.reset();
}
async refreshHCaptcha() {
window.hcaptcha.reset();
window.hcaptcha.execute();
}
async renderTurnstileFrame() {
this.renderFrame(
html`<div
@ -262,13 +290,18 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
}
async executeTurnstile() {
return (window as unknown as TurnstileWindow).turnstile.render(
this.captchaDocumentContainer,
{
sitekey: this.challenge.siteKey,
callback: this.onTokenChange,
},
);
return window.turnstile.render(this.captchaDocumentContainer, {
sitekey: this.challenge.siteKey,
callback: this.onTokenChange,
});
}
async refreshTurnstileFrame() {
(this.captchaFrame.contentWindow as typeof window)?.turnstile.reset();
}
async refreshTurnstile() {
window.turnstile.reset();
}
async renderFrame(captchaElement: TemplateResult) {
@ -336,16 +369,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name));
let lastError = undefined;
let found = false;
for (const { name, interactive, execute } of handlers) {
console.debug(`authentik/stages/captcha: trying handler ${name}`);
for (const handler of handlers) {
console.debug(`authentik/stages/captcha: trying handler ${handler.name}`);
try {
const runner = this.challenge.interactive ? interactive : execute;
const runner = this.challenge.interactive
? handler.interactive
: handler.execute;
await runner.apply(this);
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
console.debug(`authentik/stages/captcha[${handler.name}]: handler succeeded`);
found = true;
this.activeHandler = handler;
break;
} catch (exc) {
console.debug(`authentik/stages/captcha[${name}]: handler failed`);
console.debug(`authentik/stages/captcha[${handler.name}]: handler failed`);
console.debug(exc);
lastError = exc;
}
@ -370,6 +406,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
document.body.appendChild(this.captchaDocumentContainer);
}
}
updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("refreshedAt") || !this.challenge) {
return;
}
console.debug("authentik/stages/captcha: refresh triggered");
if (this.challenge.interactive) {
this.activeHandler?.refreshInteractive.apply(this);
} else {
this.activeHandler?.refresh.apply(this);
}
}
}
declare global {

View File

@ -49,6 +49,8 @@ export class IdentificationStage extends BaseStage<
@state()
captchaToken = "";
@state()
captchaRefreshedAt = new Date();
static get styles(): CSSResult[] {
return [
@ -179,12 +181,16 @@ export class IdentificationStage extends BaseStage<
this.form.appendChild(totp);
}
cleanup(): void {
onSubmitSuccess(): void {
if (this.form) {
this.form.remove();
}
}
onSubmitFailure(): void {
this.captchaRefreshedAt = new Date();
}
renderSource(source: LoginSource): TemplateResult {
const icon = renderSourceIcon(source.name, source.iconUrl);
return html`<li class="pf-c-login__main-footer-links-item">
@ -287,6 +293,7 @@ export class IdentificationStage extends BaseStage<
.onTokenChange=${(token: string) => {
this.captchaToken = token;
}}
.refreshedAt=${this.captchaRefreshedAt}
embedded
></ak-stage-captcha>
`

View File

@ -184,6 +184,13 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.12
- sources: allow uuid or slug to be used for retrieving a source (#12772)
- stages/redirect: fix query parameter when redirecting to flow (cherry-pick #12750) (#12752)
## Fixed in 2024.12.4
- core: fix generic sources not being fetchable by pk (#12896)
- core: fix non-exploitable open redirect (#13696)
- flows: fix inspector permission check (#12907)
- security: fix CVE-2025-29928 (cherry-pick #13695) (#13701)
## API Changes
#### What's New

View File

@ -185,6 +185,17 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2025.2
- web/user: ensure modal container on user-settings page is min-height: 100% (cherry-pick #13402) (#13413)
- web/user: show admin interface button on mobile (cherry-pick #13421) (#13518)
## Fixed in 2025.2.3
- admin: fix system API when using bearer token (cherry-pick #13651) (#13654)
- core: fix core/user is_superuser filter (cherry-pick #13693) (#13694)
- core: fix non-exploitable open redirect, reported by [@dominic-r](https://github.com/dominic-r) (cherry-pick #13696) (#13698)
- outposts/ldap: fix paginator going into infinite loop (cherry-pick #13677) (#13679)
- security: fix CVE-2025-29928 (cherry-pick #13695) (#13700)
- stages/email: Clean newline characters in TemplateEmailMessage (cherry-pick #13666) (#13667)
- stages/identification: refresh captcha on failure (cherry-pick #13697) (#13699)
- web/admin: reworked sync status card (cherry-pick #13625) (#13692)
## API Changes
#### What's New

View File

@ -0,0 +1,23 @@
# CVE-2025-29928
## Deletion of sessions did not revoke sessions when using database session storage
### Summary
When authentik was configured to use the database for session storage (which is a non-default setting), deleting sessions via the Web Interface or the API would not revoke the session and the session holder would continue to have access to authentik.
This also affects automatic session deletion when a user is set to inactive or a user is deleted.
### Patches
authentik 2025.2.3 and 2024.12.4 fix this issue.
### Workarounds
Switching to the cache-based session storage until the authentik instance can be upgraded is recommended. This will however also delete all existing sessions and users will have to re-authenticate.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).

View File

@ -17,7 +17,6 @@
"@docusaurus/theme-common": "^3.7.0",
"@docusaurus/theme-mermaid": "^3.7.0",
"@mdx-js/react": "^3.1.0",
"@swc/core-linux-x64-gnu": "1.11.13",
"clsx": "^2.1.1",
"disqus-react": "^1.1.6",
"docusaurus-plugin-openapi-docs": "4.3.4",
@ -39,7 +38,7 @@
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.3.2",
"@types/react": "^18.3.13",
"@types/semver": "^7.5.8",
"@types/semver": "^7.7.0",
"cross-env": "^7.0.3",
"prettier": "3.5.3",
"typescript": "~5.8.2",
@ -6461,9 +6460,9 @@
}
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
"integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
"dev": true,
"license": "MIT"
},

View File

@ -54,7 +54,7 @@
]
},
"devDependencies": {
"@types/semver": "^7.5.8",
"@types/semver": "^7.7.0",
"@docusaurus/module-type-aliases": "^3.3.2",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.3.2",

View File

@ -704,6 +704,11 @@ export default {
type: "category",
label: "CVEs",
items: [
{
type: "category",
label: "2025",
items: ["security/cves/CVE-2025-29928"],
},
{
type: "category",
label: "2024",