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);
314QByteArray CSRFProtectionPrivate::saltCipherSecret(
const QByteArray &secret)
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;
346QByteArray CSRFProtectionPrivate::unsaltCipherToken(
const QByteArray &token)
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;
355 pairs.reserve(std::ranges::min(salt.
size(), _token.
size()));
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());
387QByteArray CSRFProtectionPrivate::sanitizeToken(
const QByteArray &token)
389 QByteArray sanitized;
392 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe) ||
393 token.
size() != CSRFProtectionPrivate::tokenLength) {
394 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
406QByteArray CSRFProtectionPrivate::getToken(
Context *c)
411 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
415 if (csrf->d_ptr->useSessions) {
418 QByteArray cookieToken = c->
req()->
cookie(csrf->d_ptr->cookieName);
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());
451 QNetworkCookie cookie(csrf->d_ptr->cookieName,
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) {
457 cookie.setExpirationDate(QDateTime());
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,
481 const QString &logReason,
482 const QString &displayReason)
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);
566bool CSRFProtectionPrivate::compareSaltedTokens(
const QByteArray &t1,
const QByteArray &t2)
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
'.') &&
701 refererHost == QStringView{host}.mid(1))) ||
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"));
730 QByteArray requestCsrfToken;
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
Plugin(Application *parent)
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)
Headers & headers() noexcept
void setStatus(quint16 status) noexcept
void setBody(QIODevice *body)
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) &&
QByteArray mid(qsizetype pos, qsizetype len) &&
void reserve(qsizetype size)
void resize(qsizetype newSize, char c)
qsizetype size() const const
QDateTime currentDateTime()
void append(QList< T > &&value)
bool contains(const Key &key) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
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)
QByteArray toLatin1() const const
QString host(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QByteArray toByteArray() const const