Compare commits

...

8 Commits

Author SHA1 Message Date
aba857753b release: 2023.6.2 2023-08-29 19:09:38 +02:00
022ff9b3a8 security: fix CVE-2023-39522 (#6665)
* stages/email: don't disclose whether a user exists or not when recovering

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update website

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2023/v2023.6.md
2023-08-29 19:09:10 +02:00
d6af506a78 release: 2023.6.1 2023-07-10 13:20:22 +02:00
080ac6b5bb core: fix UUID filter field for users api (#6203)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-07-10 12:14:06 +02:00
622c0faebf outposts/ldap: add test for attribute filtering (#6189)
add failing test case

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-07-09 15:11:03 +02:00
935821857a outposts/ldap: add more tests (#6188)
* outposts/ldap: add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing posixAccount

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* attempt to expand attributes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix routing without base DN

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* more logging

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove our custom attribute filtering since this is done by the ldap library

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add test for schema

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-07-09 15:11:00 +02:00
5fe737326e sources/ldap: fix more errors (#6191) 2023-07-09 15:10:57 +02:00
ff0d3c3d63 sources/ldap: fix page size (#6187)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-07-09 15:10:51 +02:00
28 changed files with 323 additions and 76 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2023.6.0
current_version = 2023.6.2
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -2,7 +2,7 @@
from os import environ
from typing import Optional
__version__ = "2023.6.0"
__version__ = "2023.6.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -15,7 +15,7 @@ from django.utils.http import urlencode
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter, UUIDFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
@ -284,7 +284,7 @@ class UsersFilter(FilterSet):
)
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
uuid = CharFilter(field_name="uuid")
uuid = UUIDFilter(field_name="uuid")
path = CharFilter(
field_name="path",

View File

@ -73,6 +73,7 @@ outposts:
ldap:
task_timeout_hours: 2
page_size: 50
tls:
ciphers: null

View File

@ -9,6 +9,7 @@ from structlog.stdlib import BoundLogger, get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
from authentik.lib.merge import MERGE_LIST_UNIQUE
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
@ -92,7 +93,7 @@ class BaseLDAPSynchronizer:
types_only=False,
get_operational_attributes=False,
controls=None,
paged_size=5,
paged_size=int(CONFIG.y("ldap.page_size", 50)),
paged_criticality=False,
):
"""Search in pages, returns each page"""

View File

@ -30,12 +30,15 @@ CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/"
def ldap_sync_all():
"""Sync all sources"""
for source in LDAPSource.objects.filter(enabled=True):
ldap_sync_single(source)
ldap_sync_single(source.pk)
@CELERY_APP.task()
def ldap_sync_single(source: LDAPSource):
def ldap_sync_single(source_pk: str):
"""Sync a single source"""
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
if not source:
return
task = chain(
# User and group sync can happen at once, they have no dependencies on each other
group(
@ -71,9 +74,8 @@ def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) ->
def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str, page_cache_key: str):
"""Synchronization of an LDAP Source"""
self.result_timeout_hours = int(CONFIG.y("ldap.task_timeout_hours"))
try:
source: LDAPSource = LDAPSource.objects.get(pk=source_pk)
except LDAPSource.DoesNotExist:
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
if not source:
# Because the source couldn't be found, we don't have a UID
# to set the state with
return

View File

@ -12,7 +12,7 @@ from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.models import FlowToken
from authentik.flows.models import FlowDesignation, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN
@ -82,6 +82,11 @@ class EmailStageView(ChallengeStageView):
"""Helper function that sends the actual email. Implies that you've
already checked that there is a pending user."""
pending_user = self.get_pending_user()
if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
# Pending user does not have a primary key, and we're in a recovery flow,
# which means the user entered an invalid identifier, so we pretend to send the
# email, to not disclose if the user exists
return
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
if not email:
email = pending_user.email

View File

@ -5,18 +5,20 @@ from unittest.mock import MagicMock, PropertyMock, patch
from django.core import mail
from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.stages.email.models import EmailStage
class TestEmailStageSending(APITestCase):
class TestEmailStageSending(FlowTestCase):
"""Email tests"""
def setUp(self):
@ -44,6 +46,13 @@ class TestEmailStageSending(APITestCase):
):
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")
events = Event.objects.filter(action=EventAction.EMAIL_SENT)
@ -54,6 +63,32 @@ class TestEmailStageSending(APITestCase):
self.assertEqual(event.context["to_email"], [self.user.email])
self.assertEqual(event.context["from_email"], "system@authentik.local")
def test_pending_fake_user(self):
"""Test with pending (fake) user"""
self.flow.designation = FlowDesignation.RECOVERY
self.flow.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = User(username=generate_id())
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
):
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 0)
def test_send_error(self):
"""Test error during sending (sending will be retried)"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

View File

@ -118,8 +118,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
username=uid_field,
email=uid_field,
)
self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
if not current_stage.show_matched_user:
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
if self.stage.executor.flow.designation == FlowDesignation.RECOVERY:
# When used in a recovery flow, always continue to not disclose if a user exists
return attrs
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
if not current_stage.password_stage:

View File

@ -188,7 +188,7 @@ class TestIdentificationStage(FlowTestCase):
],
)
def test_recovery_flow(self):
def test_link_recovery_flow(self):
"""Test that recovery flow is linked correctly"""
flow = create_test_flow()
self.stage.recovery_flow = flow
@ -226,6 +226,38 @@ class TestIdentificationStage(FlowTestCase):
],
)
def test_recovery_flow_invalid_user(self):
"""Test that an invalid user can proceed in a recovery flow"""
self.flow.designation = FlowDesignation.RECOVERY
self.flow.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
self.assertStageResponse(
response,
self.flow,
component="ak-stage-identification",
user_fields=["email"],
password_fields=False,
show_source_labels=False,
primary_action="Continue",
sources=[
{
"challenge": {
"component": "xak-flow-redirect",
"to": "/source/oauth/login/test/",
"type": ChallengeTypes.REDIRECT.value,
},
"icon_url": "/static/authentik/sources/default.svg",
"name": "test",
}
],
)
form_data = {"uid_field": generate_id()}
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
response = self.client.post(url, form_data)
self.assertEqual(response.status_code, 200)
def test_api_validate(self):
"""Test API validation"""
self.assertTrue(

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.2}
restart: unless-stopped
command: server
environment:
@ -53,7 +53,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.2}
restart: unless-stopped
command: worker
environment:

View File

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

View File

@ -40,11 +40,17 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
"name": {u.Name},
"displayName": {u.Name},
"mail": {*u.Email},
"objectClass": {constants.OCUser, constants.OCOrgPerson, constants.OCInetOrgPerson, constants.OCAKUser},
"uidNumber": {pi.GetUidNumber(u)},
"gidNumber": {pi.GetUidNumber(u)},
"homeDirectory": {fmt.Sprintf("/home/%s", u.Username)},
"sn": {u.Name},
"objectClass": {
constants.OCUser,
constants.OCOrgPerson,
constants.OCInetOrgPerson,
constants.OCAKUser,
constants.OCPosixAccount,
},
"uidNumber": {pi.GetUidNumber(u)},
"gidNumber": {pi.GetUidNumber(u)},
"homeDirectory": {fmt.Sprintf("/home/%s", u.Username)},
"sn": {u.Name},
})
return &ldap.Entry{DN: dn, Attributes: attrs}
}

View File

@ -0,0 +1,27 @@
package ldap_test
import (
"testing"
"beryju.io/ldap"
"github.com/stretchr/testify/assert"
"goauthentik.io/api/v3"
)
func Test_UserEntry(t *testing.T) {
pi := ProviderInstance()
u := api.User{
Username: "foo",
Name: "bar",
}
entry := pi.UserEntry(u)
assert.Equal(t, "cn=foo,ou=users,dc=ldap,dc=goauthentik,dc=io", entry.DN)
assert.Contains(t, entry.Attributes, &ldap.EntryAttribute{
Name: "cn",
Values: []string{u.Username},
})
assert.Contains(t, entry.Attributes, &ldap.EntryAttribute{
Name: "displayName",
Values: []string{u.Name},
})
}

View File

@ -0,0 +1,31 @@
package ldap_test
import (
"testing"
"beryju.io/ldap"
"github.com/stretchr/testify/assert"
oldap "goauthentik.io/internal/outpost/ldap"
)
func ProviderInstance() *oldap.ProviderInstance {
return &oldap.ProviderInstance{
BaseDN: "dc=ldap,dc=goauthentik,dc=io",
UserDN: "ou=users,dc=ldap,dc=goauthentik,dc=io",
VirtualGroupDN: "ou=virtual-groups,dc=ldap,dc=goauthentik,dc=io",
GroupDN: "ou=groups,dc=ldap,dc=goauthentik,dc=io",
}
}
func AssertLDAPAttributes(t *testing.T, attrs []*ldap.EntryAttribute, expected *ldap.EntryAttribute) {
found := false
for _, attr := range attrs {
if attr.Name == expected.Name {
assert.Equal(t, expected.Values, attr.Values)
found = true
}
}
if !found {
t.Fatalf("Key %s not found in ldap attributes", expected.Name)
}
}

View File

@ -22,7 +22,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
"type": "search",
"app": selectedApp,
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request")
req.Log().WithField("attributes", searchReq.Attributes).WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request")
}()
defer func() {
@ -40,10 +40,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
}
selectedApp = selectedProvider.GetAppSlug()
result, err := ls.searchRoute(req, selectedProvider)
if err != nil {
return result, nil
}
return ls.filterResultAttributes(req, result), nil
return result, err
}
func (ls *LDAPServer) fallbackRootDSE(req *search.Request) (ldap.ServerSearchResult, error) {

View File

@ -87,7 +87,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
c := api.NewAPIClient(ds.si.GetAPIClient().GetConfig())
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
scope := req.SearchRequest.Scope
scope := req.Scope
needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {

View File

@ -75,19 +75,3 @@ func (r *Request) Log() *log.Entry {
func (r *Request) RemoteAddr() string {
return utils.GetIP(r.conn.RemoteAddr())
}
func (r *Request) FilterLDAPAttributes(res ldap.ServerSearchResult, cb func(attr *ldap.EntryAttribute) bool) ldap.ServerSearchResult {
for _, e := range res.Entries {
newAttrs := []*ldap.EntryAttribute{}
for _, attr := range e.Attributes {
include := cb(attr)
if include {
newAttrs = append(newAttrs, attr)
} else {
r.Log().WithField("key", attr.Name).Trace("filtering out field based on LDAP request")
}
}
e.Attributes = newAttrs
}
return res
}

View File

@ -53,32 +53,3 @@ func (ls *LDAPServer) searchRoute(req *search.Request, pi *ProviderInstance) (ld
req.Log().Trace("routing to default")
return pi.searcher.Search(req)
}
func (ls *LDAPServer) filterResultAttributes(req *search.Request, result ldap.ServerSearchResult) ldap.ServerSearchResult {
allowedAttributes := []string{}
if len(req.Attributes) == 1 && req.Attributes[0] == constants.SearchAttributeNone {
allowedAttributes = []string{"objectClass"}
}
if len(req.Attributes) > 0 {
// Only strictly filter allowed attributes if we haven't already narrowed the attributes
// down
if len(allowedAttributes) < 1 {
allowedAttributes = req.Attributes
}
// Filter LDAP returned attributes by search requested attributes, taking "1.1"
// into consideration
return req.FilterLDAPAttributes(result, func(attr *ldap.EntryAttribute) bool {
for _, allowed := range allowedAttributes {
if allowed == constants.SearchAttributeAllUser ||
allowed == constants.SearchAttributeAllOperational {
return true
}
if strings.EqualFold(allowed, attr.Name) {
return true
}
}
return false
})
}
return result
}

View File

@ -113,7 +113,7 @@ filterwarnings = [
[tool.poetry]
name = "authentik"
version = "2023.6.0"
version = "2023.6.2"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

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

View File

@ -250,6 +250,7 @@ class TestProviderLDAP(SeleniumTestCase):
"organizationalPerson",
"inetOrgPerson",
"goauthentik.io/ldap/user",
"posixAccount",
],
"uidNumber": 2000 + o_user.pk,
"gidNumber": 2000 + o_user.pk,
@ -277,6 +278,7 @@ class TestProviderLDAP(SeleniumTestCase):
"organizationalPerson",
"inetOrgPerson",
"goauthentik.io/ldap/user",
"posixAccount",
],
"uidNumber": 2000 + embedded_account.pk,
"gidNumber": 2000 + embedded_account.pk,
@ -304,6 +306,7 @@ class TestProviderLDAP(SeleniumTestCase):
"organizationalPerson",
"inetOrgPerson",
"goauthentik.io/ldap/user",
"posixAccount",
],
"uidNumber": 2000 + self.user.pk,
"gidNumber": 2000 + self.user.pk,
@ -320,3 +323,97 @@ class TestProviderLDAP(SeleniumTestCase):
},
],
)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@reconcile_app("authentik_outposts")
def test_ldap_schema(self):
"""Test LDAP Schema"""
self._prepare()
server = Server("ldap://localhost:3389", get_info=ALL)
_connection = Connection(
server,
raise_exceptions=True,
user=f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
password=self.user.username,
)
_connection.bind()
self.assertIsNotNone(server.schema)
self.assertTrue(server.schema.is_valid())
self.assertIsNotNone(server.schema.object_classes["goauthentik.io/ldap/user"])
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@reconcile_app("authentik_outposts")
def test_ldap_search_attrs_filter(self):
"""Test search with attributes filtering"""
# Remove akadmin to ensure list is correct
# Remove user before starting container so it's not cached
User.objects.filter(username="akadmin").delete()
outpost = self._prepare()
server = Server("ldap://localhost:3389", get_info=ALL)
_connection = Connection(
server,
raise_exceptions=True,
user=f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
password=self.user.username,
)
_connection.bind()
self.assertTrue(
Event.objects.filter(
action=EventAction.LOGIN,
user={
"pk": self.user.pk,
"email": self.user.email,
"username": self.user.username,
},
)
)
embedded_account = Outpost.objects.filter(managed=MANAGED_OUTPOST).first().user
_connection.search(
"ou=Users,DC=ldaP,dc=goauthentik,dc=io",
"(objectClass=user)",
search_scope=SUBTREE,
attributes=["cn"],
)
response: dict = _connection.response
# Remove raw_attributes to make checking easier
for obj in response:
del obj["raw_attributes"]
del obj["raw_dn"]
o_user = outpost.user
self.assertCountEqual(
response,
[
{
"dn": f"cn={o_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": o_user.username,
},
"type": "searchResEntry",
},
{
"dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": embedded_account.username,
},
"type": "searchResEntry",
},
{
"dn": f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": self.user.username,
},
"type": "searchResEntry",
},
],
)

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 = "2023.6.0";
export const VERSION = "2023.6.2";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -277,6 +277,16 @@ Timeout in hours for LDAP synchronization tasks.
Defaults to `2`.
### `AUTHENTIK_LDAP__PAGE_SIZE`
:::info
Requires authentik 2023.6.1
:::
Page size for LDAP synchronization. Controls the number of objects created in a single task.
Defaults to `50`.
### `AUTHENTIK_LDAP__TLS__CIPHERS`
:::info

View File

@ -152,6 +152,10 @@ image:
- \*: fix [CVE-2023-36456](../security/CVE-2023-36456), Reported by [@thijsa](https://github.com/thijsa)
## Fixed in 2023.5.6
- \*: fix [CVE-2023-39522](../security/CVE-2023-39522), Reported by [@markrassamni](https://github.com/markrassamni)
## API Changes
#### What's Changed

View File

@ -81,6 +81,18 @@ image:
- web/user: refactor LibraryPage for testing, add CTA (#5665)
- web: Replace lingui.js with lit-localize (#5761)
## Fixed in 2023.6.1
- core: fix UUID filter field for users api (#6203)
- outposts/ldap: revert attribute filtering (#6188)
- outposts/ldap: add test for attribute filtering (#6189)
- sources/ldap: fix more errors (#6191)
- sources/ldap: fix page size (#6187)
## Fixed in 2023.6.2
- \*: fix [CVE-2023-39522](../security/CVE-2023-39522), Reported by [@markrassamni](https://github.com/markrassamni)
## API Changes
#### What's New

View File

@ -0,0 +1,27 @@
# CVE-2023-39522
_Reported by [@markrassamni](https://github.com/markrassamni)_
## Username enumeration attack
### Summary
Using a recovery flow with an identification stage an attacker is able to determine if a username exists.
### Patches
authentik 2023.5.6 and 2023.6.2 fix this issue.
### Impact
Only setups configured with a recovery flow are impacted by this.
### Details
An attacker can easily enumerate and check users' existence using the recovery flow, as a clear message is shown when a user doesn't exist. Depending on configuration this can either be done by username, email, or both.
### 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

@ -322,6 +322,7 @@ module.exports = {
},
items: [
"security/policy",
"security/CVE-2023-39522",
"security/CVE-2023-36456",
"security/2023-06-cure53",
"security/CVE-2023-26481",