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>
29 Q_LOGGING_CATEGORY(C_CSRFPROTECTION,
"cutelyst.plugin.csrfprotection", QtWarningMsg)
43 const QByteArray CSRFProtectionPrivate::allowedChars{
44 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"_qba};
45 const QString CSRFProtectionPrivate::sessionKey{u
"_csrftoken"_qs};
46 const QString CSRFProtectionPrivate::stashKeyCookie{u
"_c_csrfcookie"_qs};
47 const QString CSRFProtectionPrivate::stashKeyCookieUsed{u
"_c_csrfcookieused"_qs};
48 const QString CSRFProtectionPrivate::stashKeyCookieNeedsReset{u
"_c_csrfcookieneedsreset"_qs};
49 const QString CSRFProtectionPrivate::stashKeyCookieSet{u
"_c_csrfcookieset"_qs};
50 const QString CSRFProtectionPrivate::stashKeyProcessingDone{u
"_c_csrfprocessingdone"_qs};
51 const QString CSRFProtectionPrivate::stashKeyCheckPassed{u
"_c_csrfcheckpassed"_qs};
55 , d_ptr(new CSRFProtectionPrivate)
59 CSRFProtection::CSRFProtection(
Application *parent,
const QVariantMap &defaultConfig)
61 , d_ptr(new CSRFProtectionPrivate)
64 d->defaultConfig = defaultConfig;
67 CSRFProtection::~CSRFProtection() =
default;
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;
168 void CSRFProtection::setDefaultDetachTo(
const QString &actionNameOrPath)
171 d->defaultDetachTo = actionNameOrPath;
174 void CSRFProtection::setFormFieldName(
const QByteArray &fieldName)
177 d->formInputName = fieldName;
180 QByteArray CSRFProtection::formFieldName() noexcept
183 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
187 return csrf->d_ptr->formInputName;
190 void CSRFProtection::setErrorMsgStashKey(
const QString &keyName)
193 d->errorMsgStashKey = keyName;
196 void CSRFProtection::setIgnoredNamespaces(
const QStringList &namespaces)
199 d->ignoredNamespaces = namespaces;
202 void CSRFProtection::setUseSessions(
bool useSessions)
205 d->useSessions = useSessions;
208 void CSRFProtection::setCookieHttpOnly(
bool httpOnly)
211 d->cookieHttpOnly = httpOnly;
214 void CSRFProtection::setCookieName(
const QByteArray &cookieName)
217 d->cookieName = cookieName;
220 void CSRFProtection::setHeaderName(
const QByteArray &headerName)
223 d->headerName = headerName;
226 void CSRFProtection::setGenericErrorMessage(
const QString &message)
229 d->genericErrorMessage = message;
232 void CSRFProtection::setGenericErrorContentType(
const QByteArray &type)
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\" />")
274 bool CSRFProtection::checkPassed(
Context *c)
276 if (CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
279 return c->
stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
295 QByteArray 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::min(secret.
size(), salt.
size()));
322 for (
int i = 0; i < std::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::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));
377 QByteArray 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");
439 void 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");
480 void 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"
556 void 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];
583 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
586 CSRFProtectionPrivate::reject(c,
587 u
"CSRFProtection plugin not registered"_qs,
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);
597 CSRFProtection::getToken(c);
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"_qs,
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"_qs,
652 c->
qtTrId(
"cutelyst-csrf-reject-referer-malformed"));
655 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"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 = uri.
port(c->
req()->secure() ? httpsPort : httpPort);
680 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
684 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
685 goodHosts.
append(goodReferer);
687 QString refererHost = refererUrl.host();
688 const int refererPort = refererUrl.port(
689 refererUrl.scheme().compare(u
"https") == 0 ? httpsPort : httpPort);
690 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
694 bool refererCheck =
false;
695 for (
const auto &host : std::as_const(goodHosts)) {
696 if ((host.startsWith(u
'.') &&
697 (refererHost.
endsWith(host) || (refererHost == host.mid(1)))) ||
698 host == refererHost) {
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"_qs,
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();
746 if (requestCsrfToken.
isEmpty()) {
747 requestCsrfToken = c->
req()->
header(csrf->d_ptr->headerName);
748 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
749 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
750 <<
"from HTTP header" << csrf->d_ptr->headerName;
752 qCDebug(C_CSRFPROTECTION)
753 <<
"Can not get token from HTTP header or form field.";
756 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
757 <<
"from form field" << csrf->d_ptr->formInputName;
760 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
763 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
764 CSRFProtectionPrivate::reject(c,
765 u
"CSRF token missing or incorrect"_qs,
767 c->
qtTrId(
"cutelyst-csrf-reject-token-missin"));
774 CSRFProtectionPrivate::accept(c);
781 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
782 if (c->
stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
787 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
791 CSRFProtectionPrivate::setToken(c);
792 c->
setStash(CSRFProtectionPrivate::stashKeyCookieSet,
true);
795 #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.
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
Headers & headers() 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) 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