mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-14 01:55:25 +00:00
Fix: use a constant password mask and consolidate the no-change check
Return a constant 10 character mask instead. Move ObfuscatedPasswordField into paperless/fields.py so core no longer imports it from paperless_mail, and add is_obfuscated_password() to replace the five checks
This commit is contained in:
1 parent
b42bf50b7e
commit
13ffed0702
6 files changed
+75
-22
No files matched your search
@@ -0,0 +1,23 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
OBFUSCATED_PASSWORD_MASK = "*" * 10
|
||||
|
||||
|
||||
def is_obfuscated_password(value: str) -> bool:
|
||||
"""
|
||||
True if the value is only mask characters (or empty), meaning a client echoed
|
||||
the placeholder back rather than supplying a new secret
|
||||
"""
|
||||
return value.replace("*", "") == ""
|
||||
|
||||
|
||||
class ObfuscatedPasswordField(serializers.CharField):
|
||||
"""
|
||||
Sends a constant mask string instead of the password in the clear
|
||||
"""
|
||||
|
||||
def to_representation(self, value) -> str:
|
||||
return OBFUSCATED_PASSWORD_MASK
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return data
|
||||
@@ -20,18 +20,19 @@ from rest_framework import serializers
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
|
||||
from paperless.config import OCR_RESERVED_USER_ARGS
|
||||
from paperless.fields import ObfuscatedPasswordField
|
||||
from paperless.fields import is_obfuscated_password
|
||||
from paperless.models import ApplicationConfiguration
|
||||
from paperless.network import validate_outbound_http_url
|
||||
from paperless.validators import reject_dangerous_svg
|
||||
from paperless.validators import validate_raster_image
|
||||
from paperless_mail.serialisers import ObfuscatedPasswordField
|
||||
|
||||
logger = logging.getLogger("paperless.settings")
|
||||
|
||||
|
||||
class PasswordValidationMixin:
|
||||
def _has_real_password(self, value: str | None) -> bool:
|
||||
return bool(value) and value.replace("*", "") != ""
|
||||
return bool(value) and not is_obfuscated_password(value)
|
||||
|
||||
def validate_password(self, value: str) -> str:
|
||||
if not self._has_real_password(value):
|
||||
@@ -285,7 +286,7 @@ class ApplicationConfigurationSerializer(
|
||||
if data[field] == "":
|
||||
data[field] = None
|
||||
# Not a real value, don't overwrite the stored one
|
||||
elif len(data[field].replace("*", "")) == 0:
|
||||
elif is_obfuscated_password(data[field]):
|
||||
del data[field]
|
||||
return super().run_validation(data)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
|
||||
from paperless.fields import OBFUSCATED_PASSWORD_MASK
|
||||
from paperless.fields import ObfuscatedPasswordField
|
||||
from paperless.fields import is_obfuscated_password
|
||||
|
||||
|
||||
class TestObfuscatedPasswordField:
|
||||
@pytest.mark.parametrize(
|
||||
"secret",
|
||||
[
|
||||
"a",
|
||||
"short",
|
||||
"exactly10!",
|
||||
"a-much-longer-secret-that-should-not-leak-its-length",
|
||||
],
|
||||
)
|
||||
def test_representation_is_constant_regardless_of_length(self, secret: str) -> None:
|
||||
assert (
|
||||
ObfuscatedPasswordField().to_representation(secret)
|
||||
== OBFUSCATED_PASSWORD_MASK
|
||||
)
|
||||
|
||||
|
||||
class TestIsObfuscatedPassword:
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("", True),
|
||||
("*", True),
|
||||
("******", True),
|
||||
(OBFUSCATED_PASSWORD_MASK, True),
|
||||
("real-secret", False),
|
||||
("***x***", False),
|
||||
(" ", False),
|
||||
],
|
||||
)
|
||||
def test_detects_placeholder(self, value: str, *, expected: bool) -> None:
|
||||
assert is_obfuscated_password(value) is expected
|
||||
@@ -41,6 +41,7 @@ from documents.models import PaperlessTask
|
||||
from documents.permissions import PaperlessModelPermissions
|
||||
from documents.permissions import PaperlessObjectPermissions
|
||||
from documents.tasks import llmindex_index
|
||||
from paperless.fields import is_obfuscated_password
|
||||
from paperless.filters import GroupFilterSet
|
||||
from paperless.filters import UserFilterSet
|
||||
from paperless.models import ApplicationConfiguration
|
||||
@@ -260,7 +261,7 @@ class ProfileView(GenericAPIView[Any]):
|
||||
user = self.request.user if hasattr(self.request, "user") else None
|
||||
|
||||
password = serializer.validated_data.pop("password", None)
|
||||
if password and password.replace("*", ""):
|
||||
if password and not is_obfuscated_password(password):
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
|
||||
@@ -8,23 +8,13 @@ from documents.serialisers import CorrespondentField
|
||||
from documents.serialisers import DocumentTypeField
|
||||
from documents.serialisers import OwnedObjectSerializer
|
||||
from documents.serialisers import TagsField
|
||||
from paperless.fields import ObfuscatedPasswordField
|
||||
from paperless.fields import is_obfuscated_password
|
||||
from paperless_mail.models import MailAccount
|
||||
from paperless_mail.models import MailRule
|
||||
from paperless_mail.models import ProcessedMail
|
||||
|
||||
|
||||
class ObfuscatedPasswordField(serializers.CharField):
|
||||
"""
|
||||
Sends *** string instead of password in the clear
|
||||
"""
|
||||
|
||||
def to_representation(self, value) -> str:
|
||||
return "*" * max(10, len(value))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return data
|
||||
|
||||
|
||||
class MailAccountSerializer(OwnedObjectSerializer):
|
||||
password = ObfuscatedPasswordField()
|
||||
imap_port = serializers.IntegerField(required=True, allow_null=False)
|
||||
@@ -50,9 +40,8 @@ class MailAccountSerializer(OwnedObjectSerializer):
|
||||
]
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if (
|
||||
"password" in validated_data
|
||||
and len(validated_data.get("password").replace("*", "")) == 0
|
||||
if "password" in validated_data and is_obfuscated_password(
|
||||
validated_data["password"],
|
||||
):
|
||||
validated_data.pop("password")
|
||||
super().update(instance, validated_data)
|
||||
|
||||
@@ -30,6 +30,7 @@ from documents.permissions import PaperlessObjectPermissions
|
||||
from documents.permissions import has_perms_owner_aware
|
||||
from documents.permissions import permitted_object_ids
|
||||
from documents.views import PassUserMixin
|
||||
from paperless.fields import is_obfuscated_password
|
||||
from paperless.views import StandardPagination
|
||||
from paperless_mail.filters import ProcessedMailFilterSet
|
||||
from paperless_mail.mail import MailError
|
||||
@@ -126,9 +127,8 @@ class MailAccountViewSet(PassUserMixin, ModelViewSet[MailAccount]):
|
||||
return HttpResponseForbidden("Insufficient permissions")
|
||||
|
||||
# account exists, use the password from there instead of ***
|
||||
if (
|
||||
len(serializer.validated_data.get("password").replace("*", "")) == 0
|
||||
and existing_account is not None
|
||||
if existing_account is not None and is_obfuscated_password(
|
||||
serializer.validated_data.get("password"),
|
||||
):
|
||||
serializer.validated_data["password"] = existing_account.password
|
||||
serializer.validated_data["account_type"] = existing_account.account_type
|
||||
|
||||
Reference in new issue
Block a user