cutelyst 5.0.1
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
csrfprotection.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2017-2022 Matthias Fehring <mf@huessenbergnetz.de>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5
6#include "csrfprotection_p.h"
7
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>
20#include <algorithm>
21#include <utility>
22#include <vector>
23
24#include <QLoggingCategory>
25#include <QNetworkCookie>
26#include <QUrl>
27#include <QUuid>
28
29Q_LOGGING_CATEGORY(C_CSRFPROTECTION, "cutelyst.plugin.csrfprotection", QtWarningMsg)
30
31using namespace Cutelyst;
32using namespace Qt::Literals::StringLiterals;
33
34// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
35static thread_local CSRFProtection *csrf = nullptr;
36const QRegularExpression CSRFProtectionPrivate::sanitizeRe{u"[^a-zA-Z0-9\\-_]"_s};
37// Assume that anything not defined as 'safe' by RFC7231 needs protection
38const QByteArrayList CSRFProtectionPrivate::secureMethods = QByteArrayList({
39 "GET",
40 "HEAD",
41 "OPTIONS",
42 "TRACE",
43});
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};
53
55 : Plugin(parent)
56 , d_ptr(new CSRFProtectionPrivate)
57{
58}
59
60CSRFProtection::CSRFProtection(Application *parent, const QVariantMap &defaultConfig)
61 : Plugin(parent)
62 , d_ptr(new CSRFProtectionPrivate)
63{
64 Q_D(CSRFProtection);
65 d->defaultConfig = defaultConfig;
66}
67
69
71{
72 Q_D(CSRFProtection);
73
74 app->loadTranslations(u"plugin_csrfprotection"_s);
75
76 const QVariantMap config = app->engine()->config(u"Cutelyst_CSRFProtection_Plugin"_s);
77
78 bool cookieExpirationOk = false;
79 const QString cookieExpireStr =
80 config
81 .value(u"cookie_expiration"_s,
82 config.value(
83 u"cookie_age"_s,
84 d->defaultConfig.value(
85 u"cookie_expiration"_s,
86 static_cast<qint64>(std::chrono::duration_cast<std::chrono::seconds>(
87 CSRFProtectionPrivate::cookieDefaultExpiration)
88 .count()))))
89 .toString();
90 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
91 Utils::durationFromString(cookieExpireStr, &cookieExpirationOk));
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;
97#else
98 << "1 year";
99#endif
100 d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
101 }
102
103 d->cookieDomain =
104 config.value(u"cookie_domain"_s, d->defaultConfig.value(u"cookie_domain"_s)).toString();
105 if (d->cookieName.isEmpty()) {
106 d->cookieName = "csrftoken";
107 }
108 d->cookiePath = u"/"_s;
109
110 const QString _sameSite = config
111 .value(u"cookie_same_site"_s,
112 d->defaultConfig.value(u"cookie_same_site"_s, u"strict"_s))
113 .toString();
114 if (_sameSite.compare(u"default", Qt::CaseInsensitive) == 0) {
115 d->cookieSameSite = QNetworkCookie::SameSite::Default;
116 } else if (_sameSite.compare(u"none", Qt::CaseInsensitive) == 0) {
117 d->cookieSameSite = QNetworkCookie::SameSite::None;
118 } else if (_sameSite.compare(u"lax", Qt::CaseInsensitive) == 0) {
119 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
120 } else if (_sameSite.compare(u"strict", Qt::CaseInsensitive) == 0) {
121 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
122 } else {
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;
127 }
128
129 d->cookieSecure =
130 config.value(u"cookie_secure"_s, d->defaultConfig.value(u"cookie_secure"_s, false))
131 .toBool();
132
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;
139 }
140
141 if (d->headerName.isEmpty()) {
142 d->headerName = "X_CSRFTOKEN";
143 }
144
145 d->trustedOrigins =
146 config.value(u"trusted_origins"_s, d->defaultConfig.value(u"trusted_origins"_s))
147 .toString()
148 .split(u',', Qt::SkipEmptyParts);
149 if (d->formInputName.isEmpty()) {
150 d->formInputName = "csrfprotectiontoken";
151 }
152 d->logFailedIp =
153 config.value(u"log_failed_ip"_s, d->defaultConfig.value(u"log_failed_ip"_s, false))
154 .toBool();
155 if (d->errorMsgStashKey.isEmpty()) {
156 d->errorMsgStashKey = u"error_msg"_s;
157 }
158
159 connect(app, &Application::postForked, this, [](Application *app) {
160 csrf = app->plugin<CSRFProtection *>();
161 });
162
163 connect(app, &Application::beforeDispatch, this, [d](Context *c) { d->beforeDispatch(c); });
164
165 return true;
166}
167
168void CSRFProtection::setDefaultDetachTo(const QString &actionNameOrPath)
169{
170 Q_D(CSRFProtection);
171 d->defaultDetachTo = actionNameOrPath;
172}
173
175{
176 Q_D(CSRFProtection);
177 d->formInputName = fieldName;
178}
179
181{
182 if (!csrf) {
183 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
184 return {};
185 }
186
187 return csrf->d_ptr->formInputName;
188}
189
191{
192 Q_D(CSRFProtection);
193 d->errorMsgStashKey = keyName;
194}
195
197{
198 Q_D(CSRFProtection);
199 d->ignoredNamespaces = namespaces;
200}
201
202void CSRFProtection::setUseSessions(bool useSessions)
203{
204 Q_D(CSRFProtection);
205 d->useSessions = useSessions;
206}
207
209{
210 Q_D(CSRFProtection);
211 d->cookieHttpOnly = httpOnly;
212}
213
215{
216 Q_D(CSRFProtection);
217 d->cookieName = cookieName;
218}
219
221{
222 Q_D(CSRFProtection);
223 d->headerName = headerName;
224}
225
227{
228 Q_D(CSRFProtection);
229 d->genericErrorMessage = message;
230}
231
233{
234 Q_D(CSRFProtection);
235 d->genericContentType = type;
236}
237
239{
240 QByteArray token;
241
242 const QByteArray contextCookie = c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
243 QByteArray secret;
244 if (contextCookie.isEmpty()) {
245 secret = CSRFProtectionPrivate::getNewCsrfString();
246 token = CSRFProtectionPrivate::saltCipherSecret(secret);
247 c->setStash(CSRFProtectionPrivate::stashKeyCookie, token);
248 } else {
249 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
250 token = CSRFProtectionPrivate::saltCipherSecret(secret);
251 }
252
253 c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
254
255 return token;
256}
257
259{
260 QString form;
261
262 if (!csrf) {
263 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
264 return form;
265 }
266
267 form = QStringLiteral("<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
270
271 return form;
272}
273
275{
276 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
277 return true;
278 } else {
279 return c->stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
280 }
281}
282
283// void CSRFProtection::rotateToken(Context *c)
284//{
285// c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
286// c->setStash(QString CSRFProtectionPrivate::stashKeyCookie,
287// CSRFProtectionPrivate::getNewCsrfToken());
288// c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
289// }
290
295QByteArray CSRFProtectionPrivate::getNewCsrfString()
296{
297 QByteArray csrfString;
298
299 while (csrfString.size() < CSRFProtectionPrivate::secretLength) {
300 csrfString.append(QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding |
302 }
303
304 csrfString.resize(CSRFProtectionPrivate::secretLength);
305
306 return csrfString;
307}
308
314QByteArray CSRFProtectionPrivate::saltCipherSecret(const QByteArray &secret)
315{
316 QByteArray salted;
317 salted.reserve(CSRFProtectionPrivate::tokenLength);
318
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)));
325 }
326
327 QByteArray cipher;
328 cipher.reserve(CSRFProtectionPrivate::secretLength);
329 for (const auto &p : std::as_const(pairs)) {
330 cipher.append(
331 CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
332 CSRFProtectionPrivate::allowedChars.size()]);
333 }
334
335 salted = salt + cipher;
336
337 return salted;
338}
339
346QByteArray CSRFProtectionPrivate::unsaltCipherToken(const QByteArray &token)
347{
348 QByteArray secret;
349 secret.reserve(CSRFProtectionPrivate::secretLength);
350
351 const QByteArray salt = token.left(CSRFProtectionPrivate::secretLength);
352 const QByteArray _token = token.mid(CSRFProtectionPrivate::secretLength);
353
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)));
359 }
360
361 for (const auto &p : std::as_const(pairs)) {
362 QByteArray::size_type idx = p.first - p.second;
363 if (idx < 0) {
364 idx = CSRFProtectionPrivate::allowedChars.size() + idx;
365 }
366 secret.append(CSRFProtectionPrivate::allowedChars.at(idx));
367 }
368
369 return secret;
370}
371
377QByteArray CSRFProtectionPrivate::getNewCsrfToken()
378{
379 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
380}
381
387QByteArray CSRFProtectionPrivate::sanitizeToken(const QByteArray &token)
388{
389 QByteArray sanitized;
390
391 const QString tokenString = QString::fromLatin1(token);
392 if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe) ||
393 token.size() != CSRFProtectionPrivate::tokenLength) {
394 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
395 } else {
396 sanitized = token;
397 }
398
399 return sanitized;
400}
401
406QByteArray CSRFProtectionPrivate::getToken(Context *c)
407{
408 QByteArray token;
409
410 if (!csrf) {
411 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
412 return token;
413 }
414
415 if (csrf->d_ptr->useSessions) {
416 token = Session::value(c, CSRFProtectionPrivate::sessionKey).toByteArray();
417 } else {
418 QByteArray cookieToken = c->req()->cookie(csrf->d_ptr->cookieName);
419 if (cookieToken.isEmpty()) {
420 return token;
421 }
422
423 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
424 if (token != cookieToken) {
425 c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
426 }
427 }
428
429 qCDebug(C_CSRFPROTECTION) << "Got token" << token << "from"
430 << (csrf->d_ptr->useSessions ? "sessions" : "cookie");
431
432 return token;
433}
434
439void CSRFProtectionPrivate::setToken(Context *c)
440{
441 if (!csrf) {
442 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
443 return;
444 }
445
446 if (csrf->d_ptr->useSessions) {
448 CSRFProtectionPrivate::sessionKey,
449 c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
450 } else {
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);
455 }
456 if (csrf->d_ptr->cookieExpiration.count() == 0) {
457 cookie.setExpirationDate(QDateTime());
458 } else {
459 cookie.setExpirationDate(
460 QDateTime::currentDateTime().addDuration(csrf->d_ptr->cookieExpiration));
461 }
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);
466 c->res()->setCookie(cookie);
467 c->res()->headers().pushHeader("Vary"_ba, "Cookie"_ba);
468 }
469
470 qCDebug(C_CSRFPROTECTION) << "Set token"
471 << c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
472 << "to" << (csrf->d_ptr->useSessions ? "session" : "cookie");
473}
474
480void CSRFProtectionPrivate::reject(Context *c,
481 const QString &logReason,
482 const QString &displayReason)
483{
484 c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, false);
485
486 if (!csrf) {
487 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
488 return;
489 }
490
491 if (C_CSRFPROTECTION().isWarningEnabled()) {
492 if (csrf->d_ptr->logFailedIp) {
493 qCWarning(C_CSRFPROTECTION).nospace().noquote()
494 << "Forbidden: (" << logReason << "): " << c->req()->path() << " ["
495 << c->req()->addressString() << "]";
496 } else {
497 qCWarning(C_CSRFPROTECTION).nospace().noquote()
498 << "Forbidden: (" << logReason << "): " << c->req()->path()
499 << " [IP logging disabled]";
500 }
501 }
502
503 c->res()->setStatus(Response::Forbidden);
504 c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
505
506 QString detachToCsrf = c->action()->attribute(u"CSRFDetachTo"_s);
507 if (detachToCsrf.isEmpty()) {
508 detachToCsrf = csrf->d_ptr->defaultDetachTo;
509 }
510
511 Action *detachToAction = nullptr;
512
513 if (!detachToCsrf.isEmpty()) {
514 detachToAction = c->controller()->actionFor(detachToCsrf);
515 if (!detachToAction) {
516 detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
517 }
518 if (!detachToAction) {
519 qCWarning(C_CSRFPROTECTION)
520 << "Can not find action for" << detachToCsrf << "to detach to";
521 }
522 }
523
524 if (detachToAction) {
525 c->detach(detachToAction);
526 } else {
527 c->res()->setStatus(Response::Forbidden);
528 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
529 c->res()->setBody(csrf->d_ptr->genericErrorMessage);
530 c->res()->setContentType(csrf->d_ptr->genericContentType);
531 } else {
532 //% "403 Forbidden - CSRF protection check failed"
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"
536 " <head>\n"
537 " <title>") +
538 title +
539 QStringLiteral("</title>\n"
540 " </head>\n"
541 " <body>\n"
542 " <h1>") +
543 title +
544 QStringLiteral("</h1>\n"
545 " <p>") +
546 displayReason +
547 QStringLiteral("</p>\n"
548 " </body>\n"
549 "</html>\n"));
550 c->res()->setContentType("text/html; charset=utf-8"_ba);
551 }
552 c->finalize();
553 }
554}
555
556void CSRFProtectionPrivate::accept(Context *c)
557{
558 c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, true);
559 c->setStash(CSRFProtectionPrivate::stashKeyProcessingDone, true);
560}
561
566bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
567{
568 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
569 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
570
571 // to avoid timing attack
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];
575 }
576 return diff == 0;
577}
578
583void CSRFProtectionPrivate::beforeDispatch(Context *c)
584{
585 if (!csrf) {
586 CSRFProtectionPrivate::reject(c,
587 u"CSRFProtection plugin not registered"_s,
588 //% "The CSRF protection plugin has not been registered."
589 c->qtTrId("cutelyst-csrf-reject-not-registered"));
590 return;
591 }
592
593 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
594 if (!csrfToken.isNull()) {
595 c->setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
596 } else {
598 }
599
600 if (c->stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
601 return;
602 }
603
604 if (c->action()->attributes().contains(u"CSRFIgnore"_s)) {
605 qCDebug(C_CSRFPROTECTION).noquote().nospace()
606 << "Action " << c->action()->className() << "::" << c->action()->reverse()
607 << " is ignored by the CSRF protection";
608 return;
609 }
610
611 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
612 if (!c->action()->attributes().contains(u"CSRFRequire"_s)) {
613 qCDebug(C_CSRFPROTECTION)
614 << "Namespace" << c->action()->ns() << "is ignored by the CSRF protection";
615 return;
616 }
617 }
618
619 // only check the tokens if the method is not secure, e.g. POST
620 // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
621 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
622
623 bool ok = true;
624
625 // Suppose user visits http://example.com/
626 // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
627 // https://example.com/detonate-bomb/ and submits it via JavaScript.
628 //
629 // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
630 // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
631 // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
632 // For this reason, for https://example.com/ we need additional protection that treats
633 // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
634 // Referer header is missing for same-domain requests in only about 0.2% of cases or less,
635 // so we can use strict Referer checking.
636 if (c->req()->secure()) {
637 const auto referer = c->req()->headers().referer();
638
639 if (Q_UNLIKELY(referer.isEmpty())) {
640 CSRFProtectionPrivate::reject(c,
641 u"Referer checking failed - no Referer"_s,
642 //% "Referrer checking failed - no Referrer."
643 c->qtTrId("cutelyst-csrf-reject-no-referer"));
644 ok = false;
645 } else {
646 const QUrl refererUrl(QString::fromLatin1(referer));
647 if (Q_UNLIKELY(!refererUrl.isValid())) {
648 CSRFProtectionPrivate::reject(
649 c,
650 u"Referer checking failed - Referer is malformed"_s,
651 //% "Referrer checking failed - Referrer is malformed."
652 c->qtTrId("cutelyst-csrf-reject-referer-malformed"));
653 ok = false;
654 } else {
655 if (Q_UNLIKELY(refererUrl.scheme() != u"https")) {
656 CSRFProtectionPrivate::reject(
657 c,
658 u"Referer checking failed - Referer is insecure while "
659 "host is secure"_s,
660 //% "Referrer checking failed - Referrer is insecure while host "
661 //% "is secure."
662 c->qtTrId("cutelyst-csrf-reject-refer-insecure"));
663 ok = false;
664 } else {
665 // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
666 // If not, obey the cookie rules (or those for the session cookie, if we
667 // use sessions
668 constexpr int httpPort = 80;
669 constexpr int httpsPort = 443;
670
671 const QUrl uri = c->req()->uri();
672 QString goodReferer;
673 if (!csrf->d_ptr->useSessions) {
674 goodReferer = csrf->d_ptr->cookieDomain;
675 }
676 if (goodReferer.isEmpty()) {
677 goodReferer = uri.host();
678 }
679 const int serverPort =
680 uri.port(c->req()->secure() // cppcheck-suppress knownConditionTrueFalse
681 ? httpsPort
682 : httpPort);
683 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
684 goodReferer += u':' + QString::number(serverPort);
685 }
686
687 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
688 goodHosts.append(goodReferer);
689
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)) {
694 refererHost += u':' + QString::number(refererPort);
695 }
696
697 bool refererCheck =
698 std::ranges::any_of(goodHosts, [&refererHost](const auto &host) {
699 return (host.startsWith(u'.') &&
700 (refererHost.endsWith(host) ||
701 refererHost == QStringView{host}.mid(1))) ||
702 host == refererHost;
703 });
704
705 if (Q_UNLIKELY(!refererCheck)) {
706 ok = false;
707 CSRFProtectionPrivate::reject(
708 c,
709 u"Referer checking failed - %1 does not match any "
710 "trusted origins"_s.arg(QString::fromLatin1(referer)),
711 //% "Referrer checking failed - %1 does not match any "
712 //% "trusted origin."
713 c->qtTrId("cutelyst-csrf-reject-referer-no-trust")
714 .arg(QString::fromLatin1(referer)));
715 }
716 }
717 }
718 }
719 }
720
721 if (Q_LIKELY(ok)) {
722 if (Q_UNLIKELY(csrfToken.isEmpty())) {
723 CSRFProtectionPrivate::reject(c,
724 u"CSRF cookie not set"_s,
725 //% "CSRF cookie not set."
726 c->qtTrId("cutelyst-csrf-reject-no-cookie"));
727 ok = false;
728 } else {
729
730 QByteArray requestCsrfToken;
731 // delete does not have body data
732 if (!c->req()->isDelete()) {
733 if (c->req()->contentType().compare("multipart/form-data") == 0) {
734 // everything is an upload, even our token
735 Upload *upload =
736 c->req()->upload(QString::fromLatin1(csrf->d_ptr->formInputName));
737 if (upload && upload->size() < 1024 /*FIXME*/) {
738 requestCsrfToken = upload->readAll();
739 }
740 } else {
741 requestCsrfToken =
742 c->req()
743 ->bodyParam(QString::fromLatin1(csrf->d_ptr->formInputName))
744 .toLatin1();
745 }
746 }
747
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;
753 } else {
754 qCDebug(C_CSRFPROTECTION)
755 << "Can not get token from HTTP header or form field.";
756 }
757 } else {
758 qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
759 << "from form field" << csrf->d_ptr->formInputName;
760 }
761
762 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
763
764 if (Q_UNLIKELY(
765 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
766 CSRFProtectionPrivate::reject(c,
767 u"CSRF token missing or incorrect"_s,
768 //% "CSRF token missing or incorrect."
769 c->qtTrId("cutelyst-csrf-reject-token-missin"));
770 ok = false;
771 }
772 }
773 }
774
775 if (Q_LIKELY(ok)) {
776 CSRFProtectionPrivate::accept(c);
777 }
778 }
779
780 // Set the CSRF cookie even if it's already set, so we renew
781 // the expiry timer.
782
783 if (!c->stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
784 if (c->stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
785 return;
786 }
787 }
788
789 if (!c->stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
790 return;
791 }
792
793 CSRFProtectionPrivate::setToken(c);
794 c->setStash(CSRFProtectionPrivate::stashKeyCookieSet, true);
795}
796
797#include "moc_csrfprotection.cpp"
This class represents a Cutelyst Action.
Definition action.h:35
QString ns() const noexcept
Definition action.cpp:118
QString className() const noexcept
Definition action.cpp:86
ParamsMultiMap attributes() const noexcept
Definition action.cpp:68
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition action.cpp:74
The Cutelyst application.
Definition application.h:66
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)
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
Definition component.cpp:45
The Cutelyst Context.
Definition context.h:42
void stash(const QVariantHash &unite)
Definition context.cpp:562
void detach(Action *action=nullptr)
Definition context.cpp:338
Response * res() const noexcept
Definition context.cpp:104
void setStash(const QString &key, const QVariant &value)
Definition context.cpp:213
Request * req
Definition context.h:66
Controller * controller
Definition context.h:75
QString qtTrId(const char *id, int n=-1) const
Definition context.h:657
Action * action
Definition context.h:47
Dispatcher * dispatcher() const noexcept
Definition context.cpp:140
Action * actionFor(QStringView name) const
Action * getActionByPath(QStringView path) const
QVariantMap config(const QString &entity) const
Definition engine.cpp:122
QByteArray referer() const noexcept
Definition headers.cpp:337
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition headers.cpp:489
Plugin(Application *parent)
Definition plugin.cpp:12
QString addressString() const
Definition request.cpp:40
bool isDelete() const noexcept
Definition request.cpp:354
QByteArray header(QAnyStringView key) const noexcept
Definition request.h:611
Headers headers() const noexcept
Definition request.cpp:312
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Definition request.h:571
QByteArray cookie(QAnyStringView name) const
Definition request.cpp:277
Upload * upload(QAnyStringView name) const
Definition request.h:626
void setContentType(const QByteArray &type)
Definition response.h:230
Headers & headers() noexcept
Definition response.cpp:283
void setStatus(quint16 status) noexcept
Definition response.cpp:74
void setBody(QIODevice *body)
Definition response.cpp:105
void setCookie(const QNetworkCookie &cookie)
Definition response.cpp:202
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition session.cpp:171
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition session.cpp:186
Cutelyst Upload handles file upload requests.
Definition upload.h:26
qint64 size() const override
Definition upload.cpp:141
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition utils.cpp:302
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()
QByteArray readAll()
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
CaseInsensitive
SkipEmptyParts
QString host(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QUuid createUuid()
QByteArray toByteArray() const const