cutelyst 5.0.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::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)));
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::min(salt.size(), _token.size()));
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)));
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() != QLatin1String("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 = uri.port(c->req()->secure() ? httpsPort : httpPort);
680 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
681 goodReferer += u':' + QString::number(serverPort);
682 }
683
684 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
685 goodHosts.append(goodReferer);
686
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)) {
691 refererHost += u':' + QString::number(refererPort);
692 }
693
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) {
699 refererCheck = true;
700 break;
701 }
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:119
QString className() const noexcept
Definition action.cpp:87
ParamsMultiMap attributes() const noexcept
Definition action.cpp:69
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition action.cpp:75
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:563
void detach(Action *action=nullptr)
Definition context.cpp:340
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:311
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition headers.cpp:464
Base class for Cutelyst Plugins.
Definition plugin.h:25
QString addressString() const
Definition request.cpp:40
bool isDelete() const noexcept
Definition request.cpp:355
QByteArray header(QAnyStringView key) const noexcept
Definition request.h:611
Headers headers() const noexcept
Definition request.cpp:313
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Definition request.h:571
QByteArray cookie(QAnyStringView name) const
Definition request.cpp:278
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:140
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) 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
CaseInsensitive
SkipEmptyParts
QString host(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QUuid createUuid()
QByteArray toByteArray() const const