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)
43const QByteArray CSRFProtectionPrivate::allowedChars{
44 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"_qba};
45const QString CSRFProtectionPrivate::sessionKey{u
"_csrftoken"_qs};
46const QString CSRFProtectionPrivate::stashKeyCookie{u
"_c_csrfcookie"_qs};
47const QString CSRFProtectionPrivate::stashKeyCookieUsed{u
"_c_csrfcookieused"_qs};
48const QString CSRFProtectionPrivate::stashKeyCookieNeedsReset{u
"_c_csrfcookieneedsreset"_qs};
49const QString CSRFProtectionPrivate::stashKeyCookieSet{u
"_c_csrfcookieset"_qs};
50const QString CSRFProtectionPrivate::stashKeyProcessingDone{u
"_c_csrfprocessingdone"_qs};
51const QString CSRFProtectionPrivate::stashKeyCheckPassed{u
"_c_csrfcheckpassed"_qs};
55 , d_ptr(new CSRFProtectionPrivate)
61 , d_ptr(new CSRFProtectionPrivate)
64 d->defaultConfig = defaultConfig;
75 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_CSRFProtection_Plugin"_qs);
77 bool cookieExpirationOk =
false;
80 .value(u
"cookie_expiration"_qs,
83 d->defaultConfig.value(
84 u
"cookie_expiration"_qs,
85 static_cast<qint64
>(std::chrono::duration_cast<std::chrono::seconds>(
86 CSRFProtectionPrivate::cookieDefaultExpiration)
89 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
91 if (!cookieExpirationOk) {
92 qCWarning(C_CSRFPROTECTION).nospace() <<
"Invalid value set for cookie_expiration. "
93 "Using default value "
94#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
95 << CSRFProtectionPrivate::cookieDefaultExpiration;
99 d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
103 config.value(u
"cookie_domain"_qs, d->defaultConfig.value(u
"cookie_domain"_qs)).toString();
104 if (d->cookieName.isEmpty()) {
105 d->cookieName =
"csrftoken";
107 d->cookiePath = u
"/"_qs;
111 .value(u
"cookie_same_site"_qs,
112 d->defaultConfig.value(u
"cookie_same_site"_qs, u
"strict"_qs))
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"_qs, d->defaultConfig.value(u
"cookie_secure"_qs,
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"_qs, d->defaultConfig.value(u
"trusted_origins"_qs))
149 if (d->formInputName.isEmpty()) {
150 d->formInputName =
"csrfprotectiontoken";
153 config.
value(u
"log_failed_ip"_qs, d->defaultConfig.value(u
"log_failed_ip"_qs,
false))
155 if (d->errorMsgStashKey.isEmpty()) {
156 d->errorMsgStashKey = u
"error_msg"_qs;
171 d->defaultDetachTo = actionNameOrPath;
177 d->formInputName = fieldName;
183 d->errorMsgStashKey = keyName;
189 d->ignoredNamespaces = namespaces;
195 d->useSessions = useSessions;
201 d->cookieHttpOnly = httpOnly;
207 d->cookieName = cookieName;
213 d->headerName = headerName;
219 d->genericErrorMessage = message;
225 d->genericContentType = type;
232 const QByteArray contextCookie = c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
235 secret = CSRFProtectionPrivate::getNewCsrfString();
236 token = CSRFProtectionPrivate::saltCipherSecret(secret);
237 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, token);
239 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
240 token = CSRFProtectionPrivate::saltCipherSecret(secret);
243 c->
setStash(CSRFProtectionPrivate::stashKeyCookieUsed,
true);
253 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
257 form = QStringLiteral(
"<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
266 if (CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
269 return c->
stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
285QByteArray CSRFProtectionPrivate::getNewCsrfString()
289 while (csrfString.
size() < CSRFProtectionPrivate::secretLength) {
294 csrfString.
resize(CSRFProtectionPrivate::secretLength);
307 salted.
reserve(CSRFProtectionPrivate::tokenLength);
309 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
310 std::vector<std::pair<int, int>> pairs;
311 pairs.reserve(std::min(secret.
size(), salt.
size()));
312 for (
int i = 0; i < std::min(secret.
size(), salt.
size()); ++i) {
313 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.
at(i)),
314 CSRFProtectionPrivate::allowedChars.indexOf(salt.
at(i)));
318 cipher.
reserve(CSRFProtectionPrivate::secretLength);
319 for (
const auto &p : std::as_const(pairs)) {
321 CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
322 CSRFProtectionPrivate::allowedChars.size()]);
325 salted = salt + cipher;
339 secret.
reserve(CSRFProtectionPrivate::secretLength);
341 const QByteArray salt = token.
left(CSRFProtectionPrivate::secretLength);
342 const QByteArray _token = token.
mid(CSRFProtectionPrivate::secretLength);
344 std::vector<std::pair<int, int>> pairs;
346 for (
int i = 0; i < std::min(salt.
size(), _token.
size()); ++i) {
347 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.
at(i)),
348 CSRFProtectionPrivate::allowedChars.indexOf(salt.
at(i)));
351 for (
const auto &p : std::as_const(pairs)) {
352 QByteArray::size_type idx = p.first - p.second;
354 idx = CSRFProtectionPrivate::allowedChars.size() + idx;
356 secret.
append(CSRFProtectionPrivate::allowedChars.at(idx));
367QByteArray CSRFProtectionPrivate::getNewCsrfToken()
369 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
382 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe) ||
383 token.
size() != CSRFProtectionPrivate::tokenLength) {
384 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
401 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
405 if (csrf->d_ptr->useSessions) {
413 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
414 if (token != cookieToken) {
415 c->
setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset,
true);
419 qCDebug(C_CSRFPROTECTION) <<
"Got token" << token <<
"from"
420 << (csrf->d_ptr->useSessions ?
"sessions" :
"cookie");
429void CSRFProtectionPrivate::setToken(
Context *c)
432 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
436 if (csrf->d_ptr->useSessions) {
438 CSRFProtectionPrivate::sessionKey,
439 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
442 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
443 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
444 cookie.setDomain(csrf->d_ptr->cookieDomain);
446 if (csrf->d_ptr->cookieExpiration.count() == 0) {
449#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
450 cookie.setExpirationDate(
453 cookie.setExpirationDate(
457 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
458 cookie.setPath(csrf->d_ptr->cookiePath);
459 cookie.setSecure(csrf->d_ptr->cookieSecure);
460 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
465 qCDebug(C_CSRFPROTECTION) <<
"Set token"
466 << c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
467 <<
"to" << (csrf->d_ptr->useSessions ?
"session" :
"cookie");
475void CSRFProtectionPrivate::reject(
Context *c,
479 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
false);
482 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
486 if (C_CSRFPROTECTION().isWarningEnabled()) {
487 if (csrf->d_ptr->logFailedIp) {
488 qCWarning(C_CSRFPROTECTION).nospace().noquote()
489 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path() <<
" ["
492 qCWarning(C_CSRFPROTECTION).nospace().noquote()
493 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path()
494 <<
" [IP logging disabled]";
499 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
503 detachToCsrf = csrf->d_ptr->defaultDetachTo;
506 Action *detachToAction =
nullptr;
510 if (!detachToAction) {
513 if (!detachToAction) {
514 qCWarning(C_CSRFPROTECTION)
515 <<
"Can not find action for" << detachToCsrf <<
"to detach to";
519 if (detachToAction) {
520 c->
detach(detachToAction);
523 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
524 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
528 const QString title = c->
qtTrId(
"cutelyst-csrf-generic-error-page-title");
529 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
530 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
534 QStringLiteral(
"</title>\n"
539 QStringLiteral(
"</h1>\n"
542 QStringLiteral(
"</p>\n"
551void CSRFProtectionPrivate::accept(
Context *c)
553 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
true);
554 c->
setStash(CSRFProtectionPrivate::stashKeyProcessingDone,
true);
563 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
564 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
567 QByteArray::size_type diff = _t1.
size() ^ _t2.
size();
568 for (QByteArray::size_type i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
569 diff |= _t1[i] ^ _t2[i];
578void CSRFProtectionPrivate::beforeDispatch(
Context *c)
581 CSRFProtectionPrivate::reject(c,
582 u
"CSRFProtection plugin not registered"_qs,
584 c->
qtTrId(
"cutelyst-csrf-reject-not-registered"));
588 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
589 if (!csrfToken.
isNull()) {
590 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
595 if (c->
stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
600 qCDebug(C_CSRFPROTECTION).noquote().nospace()
602 <<
" is ignored by the CSRF protection";
606 if (csrf->d_ptr->ignoredNamespaces.contains(c->
action()->
ns())) {
608 qCDebug(C_CSRFPROTECTION)
609 <<
"Namespace" << c->
action()->
ns() <<
"is ignored by the CSRF protection";
616 if (!CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
631 if (c->
req()->secure()) {
634 if (Q_UNLIKELY(referer.isEmpty())) {
635 CSRFProtectionPrivate::reject(c,
636 u
"Referer checking failed - no Referer"_qs,
638 c->
qtTrId(
"cutelyst-csrf-reject-no-referer"));
642 if (Q_UNLIKELY(!refererUrl.isValid())) {
643 CSRFProtectionPrivate::reject(
645 u
"Referer checking failed - Referer is malformed"_qs,
647 c->
qtTrId(
"cutelyst-csrf-reject-referer-malformed"));
650 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"https"))) {
651 CSRFProtectionPrivate::reject(
653 u
"Referer checking failed - Referer is insecure while "
657 c->
qtTrId(
"cutelyst-csrf-reject-refer-insecure"));
663 constexpr int httpPort = 80;
664 constexpr int httpsPort = 443;
666 const QUrl uri = c->
req()->uri();
668 if (!csrf->d_ptr->useSessions) {
669 goodReferer = csrf->d_ptr->cookieDomain;
672 goodReferer = uri.
host();
674 const int serverPort = uri.
port(c->
req()->secure() ? httpsPort : httpPort);
675 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
679 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
680 goodHosts.
append(goodReferer);
682 QString refererHost = refererUrl.host();
683 const int refererPort = refererUrl.port(
684 refererUrl.scheme().compare(u
"https") == 0 ? httpsPort : httpPort);
685 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
689 bool refererCheck =
false;
690 for (
const auto &host : std::as_const(goodHosts)) {
691 if ((host.startsWith(u
'.') &&
692 (refererHost.
endsWith(host) || (refererHost == host.mid(1)))) ||
693 host == refererHost) {
699 if (Q_UNLIKELY(!refererCheck)) {
701 CSRFProtectionPrivate::reject(
703 u
"Referer checking failed - %1 does not match any "
707 c->
qtTrId(
"cutelyst-csrf-reject-referer-no-trust")
716 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
717 CSRFProtectionPrivate::reject(c,
718 u
"CSRF cookie not set"_qs,
720 c->
qtTrId(
"cutelyst-csrf-reject-no-cookie"));
727 if (c->
req()->contentType().
compare(
"multipart/form-data") == 0) {
731 if (upload && upload->
size() < 1024 ) {
732 requestCsrfToken = upload->
readAll();
741 if (requestCsrfToken.
isEmpty()) {
742 requestCsrfToken = c->
req()->
header(csrf->d_ptr->headerName);
743 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
744 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
745 <<
"from HTTP header" << csrf->d_ptr->headerName;
747 qCDebug(C_CSRFPROTECTION)
748 <<
"Can not get token from HTTP header or form field.";
751 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
752 <<
"from form field" << csrf->d_ptr->formInputName;
755 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
758 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
759 CSRFProtectionPrivate::reject(c,
760 u
"CSRF token missing or incorrect"_qs,
762 c->
qtTrId(
"cutelyst-csrf-reject-token-missin"));
769 CSRFProtectionPrivate::accept(c);
776 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
777 if (c->
stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
782 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
786 CSRFProtectionPrivate::setToken(c);
787 c->
setStash(CSRFProtectionPrivate::stashKeyCookieSet,
true);
790#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 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 cookie(QByteArrayView name) const
Upload * upload(QStringView name) const
Headers headers() const noexcept
QString bodyParam(const QString &key, const QString &defaultValue={}) const
QByteArray header(QByteArrayView key) const noexcept
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_LIBRARY 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
QString host(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QByteArray toByteArray() const const