6#include "csrfprotection_p.h"
8#include <Cutelyst/Action>
9#include <Cutelyst/Application>
10#include <Cutelyst/Context>
11#include <Cutelyst/Controller>
12#include <Cutelyst/Dispatcher>
13#include <Cutelyst/Engine>
14#include <Cutelyst/Headers>
15#include <Cutelyst/Plugins/Session/Session>
16#include <Cutelyst/Request>
17#include <Cutelyst/Response>
18#include <Cutelyst/Upload>
19#include <Cutelyst/utils.h>
24#include <QLoggingCategory>
25#include <QNetworkCookie>
29Q_LOGGING_CATEGORY(C_CSRFPROTECTION,
"cutelyst.plugin.csrfprotection", QtWarningMsg)
44const QByteArray CSRFProtectionPrivate::allowedChars{
45 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"_ba};
46const QString CSRFProtectionPrivate::sessionKey{u
"_csrftoken"_s};
47const QString CSRFProtectionPrivate::stashKeyCookie{u
"_c_csrfcookie"_s};
48const QString CSRFProtectionPrivate::stashKeyCookieUsed{u
"_c_csrfcookieused"_s};
49const QString CSRFProtectionPrivate::stashKeyCookieNeedsReset{u
"_c_csrfcookieneedsreset"_s};
50const QString CSRFProtectionPrivate::stashKeyCookieSet{u
"_c_csrfcookieset"_s};
51const QString CSRFProtectionPrivate::stashKeyProcessingDone{u
"_c_csrfprocessingdone"_s};
52const QString CSRFProtectionPrivate::stashKeyCheckPassed{u
"_c_csrfcheckpassed"_s};
56 , d_ptr(new CSRFProtectionPrivate)
62 , d_ptr(new CSRFProtectionPrivate)
65 d->defaultConfig = defaultConfig;
76 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_CSRFProtection_Plugin"_s);
78 bool cookieExpirationOk =
false;
81 .value(u
"cookie_expiration"_s,
84 d->defaultConfig.value(
85 u
"cookie_expiration"_s,
86 static_cast<qint64
>(std::chrono::duration_cast<std::chrono::seconds>(
87 CSRFProtectionPrivate::cookieDefaultExpiration)
90 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
92 if (!cookieExpirationOk) {
93 qCWarning(C_CSRFPROTECTION).nospace() <<
"Invalid value set for cookie_expiration. "
94 "Using default value "
95#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
96 << CSRFProtectionPrivate::cookieDefaultExpiration;
100 d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
104 config.value(u
"cookie_domain"_s, d->defaultConfig.value(u
"cookie_domain"_s)).toString();
105 if (d->cookieName.isEmpty()) {
106 d->cookieName =
"csrftoken";
108 d->cookiePath = u
"/"_s;
110 const QString _sameSite = config
111 .value(u
"cookie_same_site"_s,
112 d->defaultConfig.value(u
"cookie_same_site"_s, u
"strict"_s))
115 d->cookieSameSite = QNetworkCookie::SameSite::Default;
117 d->cookieSameSite = QNetworkCookie::SameSite::None;
119 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
121 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
123 qCWarning(C_CSRFPROTECTION).nospace() <<
"Invalid value set for cookie_same_site. "
124 "Using default value "
125 << QNetworkCookie::SameSite::Strict;
126 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
130 config.value(u
"cookie_secure"_s, d->defaultConfig.value(u
"cookie_secure"_s,
false))
133 if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
134 qCWarning(C_CSRFPROTECTION)
135 <<
"cookie_same_site has been set to None but cookie_secure is "
136 "not set to true. Implicitely setting cookie_secure to true. "
137 "Please check your configuration.";
138 d->cookieSecure =
true;
141 if (d->headerName.isEmpty()) {
142 d->headerName =
"X_CSRFTOKEN";
146 config.value(u
"trusted_origins"_s, d->defaultConfig.value(u
"trusted_origins"_s))
149 if (d->formInputName.isEmpty()) {
150 d->formInputName =
"csrfprotectiontoken";
153 config.
value(u
"log_failed_ip"_s, d->defaultConfig.value(u
"log_failed_ip"_s,
false))
155 if (d->errorMsgStashKey.isEmpty()) {
156 d->errorMsgStashKey = u
"error_msg"_s;
171 d->defaultDetachTo = actionNameOrPath;
177 d->formInputName = fieldName;
183 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
187 return csrf->d_ptr->formInputName;
193 d->errorMsgStashKey = keyName;
199 d->ignoredNamespaces = namespaces;
205 d->useSessions = useSessions;
211 d->cookieHttpOnly = httpOnly;
217 d->cookieName = cookieName;
223 d->headerName = headerName;
229 d->genericErrorMessage = message;
235 d->genericContentType = type;
242 const QByteArray contextCookie = c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
245 secret = CSRFProtectionPrivate::getNewCsrfString();
246 token = CSRFProtectionPrivate::saltCipherSecret(secret);
247 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, token);
249 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
250 token = CSRFProtectionPrivate::saltCipherSecret(secret);
253 c->
setStash(CSRFProtectionPrivate::stashKeyCookieUsed,
true);
263 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
267 form = QStringLiteral(
"<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
276 if (CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
279 return c->
stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
295QByteArray CSRFProtectionPrivate::getNewCsrfString()
299 while (csrfString.
size() < CSRFProtectionPrivate::secretLength) {
304 csrfString.
resize(CSRFProtectionPrivate::secretLength);
317 salted.
reserve(CSRFProtectionPrivate::tokenLength);
319 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
320 std::vector<std::pair<int, int>> pairs;
321 pairs.reserve(std::ranges::min(secret.
size(), salt.
size()));
322 for (
int i = 0; i < std::ranges::min(secret.
size(), salt.
size()); ++i) {
323 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.
at(i)),
324 CSRFProtectionPrivate::allowedChars.indexOf(salt.
at(i)));
328 cipher.
reserve(CSRFProtectionPrivate::secretLength);
329 for (
const auto &p : std::as_const(pairs)) {
331 CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
332 CSRFProtectionPrivate::allowedChars.size()]);
335 salted = salt + cipher;
349 secret.
reserve(CSRFProtectionPrivate::secretLength);
351 const QByteArray salt = token.
left(CSRFProtectionPrivate::secretLength);
352 const QByteArray _token = token.
mid(CSRFProtectionPrivate::secretLength);
354 std::vector<std::pair<int, int>> pairs;
356 for (
int i = 0; i < std::ranges::min(salt.
size(), _token.
size()); ++i) {
357 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.
at(i)),
358 CSRFProtectionPrivate::allowedChars.indexOf(salt.
at(i)));
361 for (
const auto &p : std::as_const(pairs)) {
362 QByteArray::size_type idx = p.first - p.second;
364 idx = CSRFProtectionPrivate::allowedChars.size() + idx;
366 secret.
append(CSRFProtectionPrivate::allowedChars.at(idx));
377QByteArray CSRFProtectionPrivate::getNewCsrfToken()
379 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
392 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe) ||
393 token.
size() != CSRFProtectionPrivate::tokenLength) {
394 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
411 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
415 if (csrf->d_ptr->useSessions) {
423 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
424 if (token != cookieToken) {
425 c->
setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset,
true);
429 qCDebug(C_CSRFPROTECTION) <<
"Got token" << token <<
"from"
430 << (csrf->d_ptr->useSessions ?
"sessions" :
"cookie");
439void CSRFProtectionPrivate::setToken(
Context *c)
442 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
446 if (csrf->d_ptr->useSessions) {
448 CSRFProtectionPrivate::sessionKey,
449 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
452 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
453 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
454 cookie.setDomain(csrf->d_ptr->cookieDomain);
456 if (csrf->d_ptr->cookieExpiration.count() == 0) {
459 cookie.setExpirationDate(
462 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
463 cookie.setPath(csrf->d_ptr->cookiePath);
464 cookie.setSecure(csrf->d_ptr->cookieSecure);
465 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
470 qCDebug(C_CSRFPROTECTION) <<
"Set token"
471 << c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
472 <<
"to" << (csrf->d_ptr->useSessions ?
"session" :
"cookie");
480void CSRFProtectionPrivate::reject(
Context *c,
484 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
false);
487 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
491 if (C_CSRFPROTECTION().isWarningEnabled()) {
492 if (csrf->d_ptr->logFailedIp) {
493 qCWarning(C_CSRFPROTECTION).nospace().noquote()
494 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path() <<
" ["
497 qCWarning(C_CSRFPROTECTION).nospace().noquote()
498 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path()
499 <<
" [IP logging disabled]";
504 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
508 detachToCsrf = csrf->d_ptr->defaultDetachTo;
511 Action *detachToAction =
nullptr;
515 if (!detachToAction) {
518 if (!detachToAction) {
519 qCWarning(C_CSRFPROTECTION)
520 <<
"Can not find action for" << detachToCsrf <<
"to detach to";
524 if (detachToAction) {
525 c->
detach(detachToAction);
528 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
529 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
533 const QString title = c->
qtTrId(
"cutelyst-csrf-generic-error-page-title");
534 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
535 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
539 QStringLiteral(
"</title>\n"
544 QStringLiteral(
"</h1>\n"
547 QStringLiteral(
"</p>\n"
556void CSRFProtectionPrivate::accept(
Context *c)
558 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
true);
559 c->
setStash(CSRFProtectionPrivate::stashKeyProcessingDone,
true);
568 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
569 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
572 QByteArray::size_type diff = _t1.
size() ^ _t2.
size();
573 for (QByteArray::size_type i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
574 diff |= _t1[i] ^ _t2[i];
583void CSRFProtectionPrivate::beforeDispatch(
Context *c)
586 CSRFProtectionPrivate::reject(c,
587 u
"CSRFProtection plugin not registered"_s,
589 c->
qtTrId(
"cutelyst-csrf-reject-not-registered"));
593 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
594 if (!csrfToken.
isNull()) {
595 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
600 if (c->
stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
605 qCDebug(C_CSRFPROTECTION).noquote().nospace()
607 <<
" is ignored by the CSRF protection";
611 if (csrf->d_ptr->ignoredNamespaces.contains(c->
action()->
ns())) {
613 qCDebug(C_CSRFPROTECTION)
614 <<
"Namespace" << c->
action()->
ns() <<
"is ignored by the CSRF protection";
621 if (!CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
636 if (c->
req()->secure()) {
639 if (Q_UNLIKELY(referer.isEmpty())) {
640 CSRFProtectionPrivate::reject(c,
641 u
"Referer checking failed - no Referer"_s,
643 c->
qtTrId(
"cutelyst-csrf-reject-no-referer"));
647 if (Q_UNLIKELY(!refererUrl.isValid())) {
648 CSRFProtectionPrivate::reject(
650 u
"Referer checking failed - Referer is malformed"_s,
652 c->
qtTrId(
"cutelyst-csrf-reject-referer-malformed"));
655 if (Q_UNLIKELY(refererUrl.scheme() != u
"https")) {
656 CSRFProtectionPrivate::reject(
658 u
"Referer checking failed - Referer is insecure while "
662 c->
qtTrId(
"cutelyst-csrf-reject-refer-insecure"));
668 constexpr int httpPort = 80;
669 constexpr int httpsPort = 443;
671 const QUrl uri = c->
req()->uri();
673 if (!csrf->d_ptr->useSessions) {
674 goodReferer = csrf->d_ptr->cookieDomain;
677 goodReferer = uri.
host();
679 const int serverPort =
683 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
687 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
688 goodHosts.
append(goodReferer);
690 QString refererHost = refererUrl.host();
691 const int refererPort = refererUrl.port(
692 refererUrl.scheme().compare(u
"https") == 0 ? httpsPort : httpPort);
693 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
698 std::ranges::any_of(goodHosts, [&refererHost](
const auto &host) {
699 return (host.startsWith(u
'.') &&
705 if (Q_UNLIKELY(!refererCheck)) {
707 CSRFProtectionPrivate::reject(
709 u
"Referer checking failed - %1 does not match any "
713 c->
qtTrId(
"cutelyst-csrf-reject-referer-no-trust")
722 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
723 CSRFProtectionPrivate::reject(c,
724 u
"CSRF cookie not set"_s,
726 c->
qtTrId(
"cutelyst-csrf-reject-no-cookie"));
733 if (c->
req()->contentType().
compare(
"multipart/form-data") == 0) {
737 if (upload && upload->
size() < 1024 ) {
738 requestCsrfToken = upload->
readAll();
748 if (requestCsrfToken.
isEmpty()) {
749 requestCsrfToken = c->
req()->
header(csrf->d_ptr->headerName);
750 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
751 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
752 <<
"from HTTP header" << csrf->d_ptr->headerName;
754 qCDebug(C_CSRFPROTECTION)
755 <<
"Can not get token from HTTP header or form field.";
758 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
759 <<
"from form field" << csrf->d_ptr->formInputName;
762 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
765 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
766 CSRFProtectionPrivate::reject(c,
767 u
"CSRF token missing or incorrect"_s,
769 c->
qtTrId(
"cutelyst-csrf-reject-token-missin"));
776 CSRFProtectionPrivate::accept(c);
783 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
784 if (c->
stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
789 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
793 CSRFProtectionPrivate::setToken(c);
794 c->
setStash(CSRFProtectionPrivate::stashKeyCookieSet,
true);
797#include "moc_csrfprotection.cpp"
This class represents a Cutelyst Action.
QString ns() const noexcept
QString className() const noexcept
ParamsMultiMap attributes() const noexcept
QString attribute(const QString &name, const QString &defaultValue={}) const
The Cutelyst application.
Engine * engine() const noexcept
void beforeDispatch(Cutelyst::Context *c)
void loadTranslations(const QString &filename, const QString &directory={}, const QString &prefix={}, const QString &suffix={})
void postForked(Cutelyst::Application *app)
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
static QByteArray formFieldName() noexcept
static bool checkPassed(Context *c)
void setUseSessions(bool useSessions)
void setIgnoredNamespaces(const QStringList &namespaces)
void setDefaultDetachTo(const QString &actionNameOrPath)
void setGenericErrorContentType(const QByteArray &type)
void setHeaderName(const QByteArray &headerName)
void setErrorMsgStashKey(const QString &keyName)
void setFormFieldName(const QByteArray &fieldName)
void setCookieHttpOnly(bool httpOnly)
static QByteArray getToken(Context *c)
~CSRFProtection() override
void setGenericErrorMessage(const QString &message)
bool setup(Application *app) override
void setCookieName(const QByteArray &cookieName)
static QString getTokenFormField(Context *c)
CSRFProtection(Application *parent)
QString reverse() const noexcept
void stash(const QVariantHash &unite)
void detach(Action *action=nullptr)
Response * res() const noexcept
void setStash(const QString &key, const QVariant &value)
QString qtTrId(const char *id, int n=-1) const
Dispatcher * dispatcher() const noexcept
Action * actionFor(QStringView name) const
Action * getActionByPath(QStringView path) const
QVariantMap config(const QString &entity) const
Base class for Cutelyst Plugins.
QString addressString() const
bool isDelete() const noexcept
QByteArray header(QAnyStringView key) const noexcept
Headers headers() const noexcept
QString bodyParam(const QString &key, const QString &defaultValue={}) const
QByteArray cookie(QAnyStringView name) const
Upload * upload(QAnyStringView name) const
void setContentType(const QByteArray &type)
void setStatus(quint16 status) noexcept
void setBody(QIODevice *body)
Headers & headers() noexcept
void setCookie(const QNetworkCookie &cookie)
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
static void setValue(Context *c, const QString &key, const QVariant &value)
Cutelyst Upload handles file upload requests.
qint64 size() const override
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
The Cutelyst namespace holds all public Cutelyst API.
QByteArray & append(QByteArrayView data)
char at(qsizetype i) const const
int compare(QByteArrayView bv, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool isNull() const const
QByteArray left(qsizetype len) const const
QByteArray mid(qsizetype pos, qsizetype len) const const
void reserve(qsizetype size)
void resize(qsizetype newSize, char c)
qsizetype size() const const
QDateTime currentDateTime()
void append(QList::parameter_type value)
T value(qsizetype i) const const
bool contains(const Key &key) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString arg(Args &&... args) const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
QStringView mid(qsizetype start, qsizetype length) const const
QString host(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QByteArray toByteArray() const const