cutelyst 5.1.0
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()
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() << " [client "
495 << c->req()->addressString() << "]";
496 } else {
497 qCWarning(C_CSRFPROTECTION).nospace().noquote()
498 << "Forbidden: (" << logReason << "): " << c->req()->path();
499 }
500 }
501
502 c->res()->setStatus(Response::Forbidden);
503 c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
504
505 QString detachToCsrf = c->action()->attribute(u"CSRFDetachTo"_s);
506 if (detachToCsrf.isEmpty()) {
507 detachToCsrf = csrf->d_ptr->defaultDetachTo;
508 }
509
510 Action *detachToAction = nullptr;
511
512 if (!detachToCsrf.isEmpty()) {
513 detachToAction = c->controller()->actionFor(detachToCsrf);
514 if (!detachToAction) {
515 detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
516 }
517 if (!detachToAction) {
518 qCWarning(C_CSRFPROTECTION)
519 << "Can not find action for" << detachToCsrf << "to detach to";
520 }
521 }
522
523 if (detachToAction) {
524 c->detach(detachToAction);
525 } else {
526 c->res()->setStatus(Response::Forbidden);
527 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
528 c->res()->setBody(csrf->d_ptr->genericErrorMessage);
529 c->res()->setContentType(csrf->d_ptr->genericContentType);
530 } else {
531 //% "403 Forbidden - CSRF protection check failed"
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"
535 " <head>\n"
536 " <title>") +
537 title +
538 QStringLiteral("</title>\n"
539 " </head>\n"
540 " <body>\n"
541 " <h1>") +
542 title +
543 QStringLiteral("</h1>\n"
544 " <p>") +
545 displayReason +
546 QStringLiteral("</p>\n"
547 " </body>\n"
548 "</html>\n"));
549 c->res()->setContentType("text/html; charset=utf-8"_ba);
550 }
551 c->finalize();
552 }
553}
554
555void CSRFProtectionPrivate::accept(Context *c)
556{
557 c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, true);
558 c->setStash(CSRFProtectionPrivate::stashKeyProcessingDone, true);
559}
560
565bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
566{
567 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
568 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
569
570 // to avoid timing attack
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];
574 }
575 return diff == 0;
576}
577
582void CSRFProtectionPrivate::beforeDispatch(Context *c)
583{
584 if (!csrf) {
585 CSRFProtectionPrivate::reject(c,
586 u"CSRFProtection plugin not registered"_s,
587 //% "The CSRF protection plugin has not been registered."
588 c->qtTrId("cutelyst-csrf-reject-not-registered"));
589 return;
590 }
591
592 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
593 if (!csrfToken.isNull()) {
594 c->setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
595 } else {
597 }
598
599 if (c->stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
600 return;
601 }
602
603 if (c->action()->attributes().contains(u"CSRFIgnore"_s)) {
604 qCDebug(C_CSRFPROTECTION).noquote().nospace()
605 << "Action " << c->action()->className() << "::" << c->action()->reverse()
606 << " is ignored by the CSRF protection";
607 return;
608 }
609
610 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
611 if (!c->action()->attributes().contains(u"CSRFRequire"_s)) {
612 qCDebug(C_CSRFPROTECTION)
613 << "Namespace" << c->action()->ns() << "is ignored by the CSRF protection";
614 return;
615 }
616 }
617
618 // only check the tokens if the method is not secure, e.g. POST
619 // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
620 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
621
622 bool ok = true;
623
624 // Suppose user visits http://example.com/
625 // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
626 // https://example.com/detonate-bomb/ and submits it via JavaScript.
627 //
628 // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
629 // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
630 // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
631 // For this reason, for https://example.com/ we need additional protection that treats
632 // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
633 // Referer header is missing for same-domain requests in only about 0.2% of cases or less,
634 // so we can use strict Referer checking.
635 if (c->req()->secure()) {
636 const auto referer = c->req()->headers().referer();
637
638 if (Q_UNLIKELY(referer.isEmpty())) {
639 CSRFProtectionPrivate::reject(c,
640 u"Referer checking failed - no Referer"_s,
641 //% "Referrer checking failed - no Referrer."
642 c->qtTrId("cutelyst-csrf-reject-no-referer"));
643 ok = false;
644 } else {
645 const QUrl refererUrl(QString::fromLatin1(referer));
646 if (Q_UNLIKELY(!refererUrl.isValid())) {
647 CSRFProtectionPrivate::reject(
648 c,
649 u"Referer checking failed - Referer is malformed"_s,
650 //% "Referrer checking failed - Referrer is malformed."
651 c->qtTrId("cutelyst-csrf-reject-referer-malformed"));
652 ok = false;
653 } else {
654 if (Q_UNLIKELY(refererUrl.scheme() != u"https")) {
655 CSRFProtectionPrivate::reject(
656 c,
657 u"Referer checking failed - Referer is insecure while "
658 "host is secure"_s,
659 //% "Referrer checking failed - Referrer is insecure while host "
660 //% "is secure."
661 c->qtTrId("cutelyst-csrf-reject-refer-insecure"));
662 ok = false;
663 } else {
664 // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
665 // If not, obey the cookie rules (or those for the session cookie, if we
666 // use sessions
667 constexpr int httpPort = 80;
668 constexpr int httpsPort = 443;
669
670 const QUrl uri = c->req()->uri();
671 QString goodReferer;
672 if (!csrf->d_ptr->useSessions) {
673 goodReferer = csrf->d_ptr->cookieDomain;
674 }
675 if (goodReferer.isEmpty()) {
676 goodReferer = uri.host();
677 }
678 const int serverPort =
679 uri.port(c->req()->secure() // cppcheck-suppress knownConditionTrueFalse
680 ? httpsPort
681 : httpPort);
682 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
683 goodReferer += u':' + QString::number(serverPort);
684 }
685
686 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
687 goodHosts.append(goodReferer);
688
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)) {
693 refererHost += u':' + QString::number(refererPort);
694 }
695
696 bool refererCheck =
697 std::ranges::any_of(goodHosts, [&refererHost](const auto &host) {
698 return (host.startsWith(u'.') &&
699 (refererHost.endsWith(host) ||
700 refererHost == QStringView{host}.mid(1))) ||
701 host == refererHost;
702 });
703
704 if (Q_UNLIKELY(!refererCheck)) {
705 ok = false;
706 CSRFProtectionPrivate::reject(
707 c,
708 u"Referer checking failed - %1 does not match any "
709 "trusted origins"_s.arg(QString::fromLatin1(referer)),
710 //% "Referrer checking failed - %1 does not match any "
711 //% "trusted origin."
712 c->qtTrId("cutelyst-csrf-reject-referer-no-trust")
713 .arg(QString::fromLatin1(referer)));
714 }
715 }
716 }
717 }
718 }
719
720 if (Q_LIKELY(ok)) {
721 if (Q_UNLIKELY(csrfToken.isEmpty())) {
722 CSRFProtectionPrivate::reject(c,
723 u"CSRF cookie not set"_s,
724 //% "CSRF cookie not set."
725 c->qtTrId("cutelyst-csrf-reject-no-cookie"));
726 ok = false;
727 } else {
728
729 QByteArray requestCsrfToken;
730 // delete does not have body data
731 if (!c->req()->isDelete()) {
732 if (c->req()->contentType().compare("multipart/form-data") == 0) {
733 // everything is an upload, even our token
734 Upload *upload =
735 c->req()->upload(QString::fromLatin1(csrf->d_ptr->formInputName));
736 if (upload && upload->size() < 1024 /*FIXME*/) {
737 requestCsrfToken = upload->readAll();
738 }
739 } else {
740 requestCsrfToken =
741 c->req()
742 ->bodyParam(QString::fromLatin1(csrf->d_ptr->formInputName))
743 .toLatin1();
744 }
745 }
746
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;
752 } else {
753 qCDebug(C_CSRFPROTECTION)
754 << "Can not get token from HTTP header or form field.";
755 }
756 } else {
757 qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
758 << "from form field" << csrf->d_ptr->formInputName;
759 }
760
761 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
762
763 if (Q_UNLIKELY(
764 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
765 CSRFProtectionPrivate::reject(c,
766 u"CSRF token missing or incorrect"_s,
767 //% "CSRF token missing or incorrect."
768 c->qtTrId("cutelyst-csrf-reject-token-missin"));
769 ok = false;
770 }
771 }
772 }
773
774 if (Q_LIKELY(ok)) {
775 CSRFProtectionPrivate::accept(c);
776 }
777 }
778
779 // Set the CSRF cookie even if it's already set, so we renew
780 // the expiry timer.
781
782 if (!c->stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
783 if (c->stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
784 return;
785 }
786 }
787
788 if (!c->stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
789 return;
790 }
791
792 CSRFProtectionPrivate::setToken(c);
793 c->setStash(CSRFProtectionPrivate::stashKeyCookieSet, true);
794}
795
796#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:334
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition headers.cpp:486
Base class for Cutelyst Plugins.
Definition plugin.h:25
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
void setStatus(quint16 status) noexcept
Definition response.cpp:74
void setBody(QIODevice *body)
Definition response.cpp:105
Headers & headers() noexcept
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:301
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()
QByteArray readAll()
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
CaseInsensitive
SkipEmptyParts
QString host(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QUuid createUuid()
QByteArray toByteArray() const const