Files
upstream-zulip/zerver/lib/captcha.py
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

106 lines
3.7 KiB
Python

import base64
import logging
from typing import Any
import orjson
from altcha.v1 import verify_solution
from django import forms
from django.conf import settings
from django.forms.renderers import BaseRenderer
from django.http import HttpRequest
from django.utils.html import format_html
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from typing_extensions import override
def captcha_enabled() -> bool:
return bool(settings.USING_CAPTCHA and settings.ALTCHA_HMAC_KEY)
class AltchaWidget(forms.TextInput):
@override
def render(
self,
name: str,
value: Any,
attrs: dict[str, Any] | None = None,
renderer: BaseRenderer | None = None,
) -> SafeString:
return format_html(
(
"<altcha-widget"
' name="captcha"'
' challengeurl="/json/antispam_challenge"'
" hidelogo"
" hidefooter"
" refetchonexpire"
' style="{}"'
' strings="{}"'
">"
),
"--altcha-max-width: 300px;",
orjson.dumps(
{
"verified": _("Verified that you're a human user!"),
"verifying": _("Verifying that you're not a bot…"),
}
).decode(),
)
class CaptchaFormMixin(forms.Form):
captcha = forms.CharField(
widget=AltchaWidget,
error_messages={"required": gettext_lazy("Validation failed, please try again.")},
)
def __init__(self, *args: Any, request: HttpRequest, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.request = request
if not captcha_enabled():
del self.fields["captcha"]
@override
def clean(self) -> None:
super().clean()
# Validating the payload consumes the solved challenge, to
# prevent replay; defer it until the rest of the form is
# valid, so that a failure in some other field does not
# invalidate a successful solve. The captcha field does not
# exist when the captcha is not enabled.
if "captcha" in self.fields and not self.errors:
try:
validate_captcha_payload(self.request, self.cleaned_data["captcha"])
except forms.ValidationError as error:
self.add_error("captcha", error)
def validate_captcha_payload(request: HttpRequest, captcha_payload: str) -> None:
if not captcha_enabled(): # nocoverage
raise forms.ValidationError(_("Challenges are not enabled."))
try:
ok, err = verify_solution(captcha_payload, settings.ALTCHA_HMAC_KEY, check_expires=True)
if not ok:
logging.warning("Invalid altcha solution: %s", err)
raise forms.ValidationError(_("Validation failed, please try again."))
except forms.ValidationError:
raise
except Exception:
logging.exception("Error while validating altcha solution")
raise forms.ValidationError(_("Validation failed, please try again."))
captcha_data = orjson.loads(base64.b64decode(captcha_payload))
challenge = captcha_data["challenge"]
session_challenges = [e[0] for e in request.session.get("altcha_challenges", [])]
if challenge not in session_challenges:
logging.warning("Expired or replayed altcha solution")
raise forms.ValidationError(_("Validation failed, please try again."))
# Remove the successful solve from the session, to prevent replay
request.session["altcha_challenges"] = [
e for e in request.session.get("altcha_challenges", []) if e[0] != challenge
]