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() <<
" [client "
497 qCWarning(C_CSRFPROTECTION).nospace().noquote()
498 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path();
503 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
507 detachToCsrf = csrf->d_ptr->defaultDetachTo;
510 Action *detachToAction =
nullptr;
514 if (!detachToAction) {
517 if (!detachToAction) {
518 qCWarning(C_CSRFPROTECTION)
519 <<
"Can not find action for" << detachToCsrf <<
"to detach to";
523 if (detachToAction) {
524 c->
detach(detachToAction);
527 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
528 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
532 const QString title = c->
qtTrId(
"cutelyst-csrf-generic-error-page-title");
533 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
534 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
538 QStringLiteral(
"</title>\n"
543 QStringLiteral(
"</h1>\n"
546 QStringLiteral(
"</p>\n"
555void CSRFProtectionPrivate::accept(
Context *c)
557 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
true);
558 c->
setStash(CSRFProtectionPrivate::stashKeyProcessingDone,
true);
567 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
568 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
571 QByteArray::size_type diff = _t1.
size() ^ _t2.
size();
572 for (QByteArray::size_type i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
573 diff |= _t1[i] ^ _t2[i];
582void CSRFProtectionPrivate::beforeDispatch(
Context *c)
585 CSRFProtectionPrivate::reject(c,
586 u
"CSRFProtection plugin not registered"_s,
588 c->
qtTrId(
"cutelyst-csrf-reject-not-registered"));
592 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
593 if (!csrfToken.
isNull()) {
594 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
599 if (c->
stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
604 qCDebug(C_CSRFPROTECTION).noquote().nospace()
606 <<
" is ignored by the CSRF protection";
610 if (csrf->d_ptr->ignoredNamespaces.contains(c->
action()->
ns())) {
612 qCDebug(C_CSRFPROTECTION)
613 <<
"Namespace" << c->
action()->
ns() <<
"is ignored by the CSRF protection";
620 if (!CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
635 if (c->
req()->secure()) {
638 if (Q_UNLIKELY(referer.isEmpty())) {
639 CSRFProtectionPrivate::reject(c,
640 u
"Referer checking failed - no Referer"_s,
642 c->
qtTrId(
"cutelyst-csrf-reject-no-referer"));
646 if (Q_UNLIKELY(!refererUrl.isValid())) {
647 CSRFProtectionPrivate::reject(
649 u
"Referer checking failed - Referer is malformed"_s,
651 c->
qtTrId(
"cutelyst-csrf-reject-referer-malformed"));
654 if (Q_UNLIKELY(refererUrl.scheme() != u
"https")) {
655 CSRFProtectionPrivate::reject(
657 u
"Referer checking failed - Referer is insecure while "
661 c->
qtTrId(
"cutelyst-csrf-reject-refer-insecure"));
667 constexpr int httpPort = 80;
668 constexpr int httpsPort = 443;
670 const QUrl uri = c->
req()->uri();
672 if (!csrf->d_ptr->useSessions) {
673 goodReferer = csrf->d_ptr->cookieDomain;
676 goodReferer = uri.
host();
678 const int serverPort =
682 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
686 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
687 goodHosts.
append(goodReferer);
689 QString refererHost = refererUrl.host();
690 const int refererPort = refererUrl.port(
691 refererUrl.scheme().compare(u
"https") == 0 ? httpsPort : httpPort);
692 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
697 std::ranges::any_of(goodHosts, [&refererHost](
const auto &host) {
698 return (host.startsWith(u
'.') &&
704 if (Q_UNLIKELY(!refererCheck)) {
706 CSRFProtectionPrivate::reject(
708 u
"Referer checking failed - %1 does not match any "
712 c->
qtTrId(
"cutelyst-csrf-reject-referer-no-trust")
721 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
722 CSRFProtectionPrivate::reject(c,
723 u
"CSRF cookie not set"_s,
725 c->
qtTrId(
"cutelyst-csrf-reject-no-cookie"));
732 if (c->
req()->contentType().
compare(
"multipart/form-data") == 0) {
736 if (upload && upload->
size() < 1024 ) {
737 requestCsrfToken = upload->
readAll();
747 if (requestCsrfToken.
isEmpty()) {
748 requestCsrfToken = c->
req()->
header(csrf->d_ptr->headerName);
749 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
750 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
751 <<
"from HTTP header" << csrf->d_ptr->headerName;
753 qCDebug(C_CSRFPROTECTION)
754 <<
"Can not get token from HTTP header or form field.";
757 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
758 <<
"from form field" << csrf->d_ptr->formInputName;
761 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
764 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
765 CSRFProtectionPrivate::reject(c,
766 u
"CSRF token missing or incorrect"_s,
768 c->
qtTrId(
"cutelyst-csrf-reject-token-missin"));
775 CSRFProtectionPrivate::accept(c);
782 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
783 if (c->
stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
788 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
792 CSRFProtectionPrivate::setToken(c);
793 c->
setStash(CSRFProtectionPrivate::stashKeyCookieSet,
true);
796#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