cutelyst 4.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;
32
33// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
34static thread_local CSRFProtection *csrf = nullptr;
35const QRegularExpression CSRFProtectionPrivate::sanitizeRe{u"[^a-zA-Z0-9\\-_]"_qs};
36// Assume that anything not defined as 'safe' by RFC7231 needs protection
37const QByteArrayList CSRFProtectionPrivate::secureMethods = QByteArrayList({
38 "GET",
39 "HEAD",
40 "OPTIONS",
41 "TRACE",
42});
43const QByteArray CSRFProtectionPrivate::allowedChars{
44 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"_qba};
45const QString CSRFProtectionPrivate::sessionKey{u"_csrftoken"_qs};
46const QString CSRFProtectionPrivate::stashKeyCookie{u"_c_csrfcookie"_qs};
47const QString CSRFProtectionPrivate::stashKeyCookieUsed{u"_c_csrfcookieused"_qs};
48const QString CSRFProtectionPrivate::stashKeyCookieNeedsReset{u"_c_csrfcookieneedsreset"_qs};
49const QString CSRFProtectionPrivate::stashKeyCookieSet{u"_c_csrfcookieset"_qs};
50const QString CSRFProtectionPrivate::stashKeyProcessingDone{u"_c_csrfprocessingdone"_qs};
51const QString CSRFProtectionPrivate::stashKeyCheckPassed{u"_c_csrfcheckpassed"_qs};
52
54 : Plugin(parent)
55 , d_ptr(new CSRFProtectionPrivate)
56{
57}
58
60
62{
63 Q_D(CSRFProtection);
64
65 app->loadTranslations(u"plugin_csrfprotection"_qs);
66
67 const QVariantMap config = app->engine()->config(u"Cutelyst_CSRFProtection_Plugin"_qs);
68
69 bool cookieExpirationOk = false;
70 const QString cookieExpireStr =
71 config
72 .value(
73 u"cookie_expiration"_qs,
74 config.value(u"cookie_age"_qs,
75 static_cast<qint64>(std::chrono::duration_cast<std::chrono::seconds>(
76 CSRFProtectionPrivate::cookieDefaultExpiration)
77 .count())))
78 .toString();
79 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
80 Utils::durationFromString(cookieExpireStr, &cookieExpirationOk));
81 if (!cookieExpirationOk) {
82 qCWarning(C_CSRFPROTECTION).nospace() << "Invalid value set for cookie_expiration. "
83 "Using default value "
84#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
85 << CSRFProtectionPrivate::cookieDefaultExpiration;
86#else
87 << "1 year";
88#endif
89 d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
90 }
91
92 d->cookieDomain = config.value(u"cookie_domain"_qs).toString();
93 if (d->cookieName.isEmpty()) {
94 d->cookieName = "csrftoken";
95 }
96 d->cookiePath = u"/"_qs;
97
98 const QString _sameSite = config.value(u"cookie_same_site"_qs, u"strict"_qs).toString();
99 if (_sameSite.compare(u"default", Qt::CaseInsensitive) == 0) {
100 d->cookieSameSite = QNetworkCookie::SameSite::Default;
101 } else if (_sameSite.compare(u"none", Qt::CaseInsensitive) == 0) {
102 d->cookieSameSite = QNetworkCookie::SameSite::None;
103 } else if (_sameSite.compare(u"lax", Qt::CaseInsensitive) == 0) {
104 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
105 } else if (_sameSite.compare(u"strict", Qt::CaseInsensitive) == 0) {
106 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
107 } else {
108 qCWarning(C_CSRFPROTECTION).nospace() << "Invalid value set for cookie_same_site. "
109 "Using default value "
110 << QNetworkCookie::SameSite::Strict;
111 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
112 }
113
114 d->cookieSecure = config.value(u"cookie_secure"_qs, false).toBool();
115
116 if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
117 qCWarning(C_CSRFPROTECTION)
118 << "cookie_same_site has been set to None but cookie_secure is "
119 "not set to true. Implicitely setting cookie_secure to true. "
120 "Please check your configuration.";
121 d->cookieSecure = true;
122 }
123
124 if (d->headerName.isEmpty()) {
125 d->headerName = "X_CSRFTOKEN";
126 }
127
128 d->trustedOrigins =
129 config.value(u"trusted_origins"_qs).toString().split(u',', Qt::SkipEmptyParts);
130 if (d->formInputName.isEmpty()) {
131 d->formInputName = "csrfprotectiontoken";
132 }
133 d->logFailedIp = config.value(u"log_failed_ip"_qs, false).toBool();
134 if (d->errorMsgStashKey.isEmpty()) {
135 d->errorMsgStashKey = u"error_msg"_qs;
136 }
137
138 connect(app, &Application::postForked, this, [](Application *app) {
139 csrf = app->plugin<CSRFProtection *>();
140 });
141
142 connect(app, &Application::beforeDispatch, this, [d](Context *c) { d->beforeDispatch(c); });
143
144 return true;
145}
146
147void CSRFProtection::setDefaultDetachTo(const QString &actionNameOrPath)
148{
149 Q_D(CSRFProtection);
150 d->defaultDetachTo = actionNameOrPath;
151}
152
153void CSRFProtection::setFormFieldName(const QByteArray &fieldName)
154{
155 Q_D(CSRFProtection);
156 d->formInputName = fieldName;
157}
158
159void CSRFProtection::setErrorMsgStashKey(const QString &keyName)
160{
161 Q_D(CSRFProtection);
162 d->errorMsgStashKey = keyName;
163}
164
165void CSRFProtection::setIgnoredNamespaces(const QStringList &namespaces)
166{
167 Q_D(CSRFProtection);
168 d->ignoredNamespaces = namespaces;
169}
170
171void CSRFProtection::setUseSessions(bool useSessions)
172{
173 Q_D(CSRFProtection);
174 d->useSessions = useSessions;
175}
176
178{
179 Q_D(CSRFProtection);
180 d->cookieHttpOnly = httpOnly;
181}
182
183void CSRFProtection::setCookieName(const QByteArray &cookieName)
184{
185 Q_D(CSRFProtection);
186 d->cookieName = cookieName;
187}
188
189void CSRFProtection::setHeaderName(const QByteArray &headerName)
190{
191 Q_D(CSRFProtection);
192 d->headerName = headerName;
193}
194
195void CSRFProtection::setGenericErrorMessage(const QString &message)
196{
197 Q_D(CSRFProtection);
198 d->genericErrorMessage = message;
199}
200
202{
203 Q_D(CSRFProtection);
204 d->genericContentType = type;
205}
206
208{
209 QByteArray token;
210
211 const QByteArray contextCookie = c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
212 QByteArray secret;
213 if (contextCookie.isEmpty()) {
214 secret = CSRFProtectionPrivate::getNewCsrfString();
215 token = CSRFProtectionPrivate::saltCipherSecret(secret);
216 c->setStash(CSRFProtectionPrivate::stashKeyCookie, token);
217 } else {
218 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
219 token = CSRFProtectionPrivate::saltCipherSecret(secret);
220 }
221
222 c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
223
224 return token;
225}
226
228{
229 QString form;
230
231 if (!csrf) {
232 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
233 return form;
234 }
235
236 form = QStringLiteral("<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
237 .arg(QString::fromLatin1(csrf->d_ptr->formInputName),
238 QString::fromLatin1(CSRFProtection::getToken(c)));
239
240 return form;
241}
242
244{
245 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
246 return true;
247 } else {
248 return c->stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
249 }
250}
251
252// void CSRFProtection::rotateToken(Context *c)
253//{
254// c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
255// c->setStash(QString CSRFProtectionPrivate::stashKeyCookie,
256// CSRFProtectionPrivate::getNewCsrfToken());
257// c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
258// }
259
264QByteArray CSRFProtectionPrivate::getNewCsrfString()
265{
266 QByteArray csrfString;
267
268 while (csrfString.size() < CSRFProtectionPrivate::secretLength) {
269 csrfString.append(QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding |
270 QByteArray::OmitTrailingEquals));
271 }
272
273 csrfString.resize(CSRFProtectionPrivate::secretLength);
274
275 return csrfString;
276}
277
283QByteArray CSRFProtectionPrivate::saltCipherSecret(const QByteArray &secret)
284{
285 QByteArray salted;
286 salted.reserve(CSRFProtectionPrivate::tokenLength);
287
288 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
289 std::vector<std::pair<int, int>> pairs;
290 pairs.reserve(std::min(secret.size(), salt.size()));
291 for (int i = 0; i < std::min(secret.size(), salt.size()); ++i) {
292 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.at(i)),
293 CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
294 }
295
296 QByteArray cipher;
297 cipher.reserve(CSRFProtectionPrivate::secretLength);
298 for (const auto &p : std::as_const(pairs)) {
299 cipher.append(
300 CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
301 CSRFProtectionPrivate::allowedChars.size()]);
302 }
303
304 salted = salt + cipher;
305
306 return salted;
307}
308
315QByteArray CSRFProtectionPrivate::unsaltCipherToken(const QByteArray &token)
316{
317 QByteArray secret;
318 secret.reserve(CSRFProtectionPrivate::secretLength);
319
320 const QByteArray salt = token.left(CSRFProtectionPrivate::secretLength);
321 const QByteArray _token = token.mid(CSRFProtectionPrivate::secretLength);
322
323 std::vector<std::pair<int, int>> pairs;
324 pairs.reserve(std::min(salt.size(), _token.size()));
325 for (int i = 0; i < std::min(salt.size(), _token.size()); ++i) {
326 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.at(i)),
327 CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
328 }
329
330 for (const auto &p : std::as_const(pairs)) {
331 QByteArray::size_type idx = p.first - p.second;
332 if (idx < 0) {
333 idx = CSRFProtectionPrivate::allowedChars.size() + idx;
334 }
335 secret.append(CSRFProtectionPrivate::allowedChars.at(idx));
336 }
337
338 return secret;
339}
340
346QByteArray CSRFProtectionPrivate::getNewCsrfToken()
347{
348 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
349}
350
356QByteArray CSRFProtectionPrivate::sanitizeToken(const QByteArray &token)
357{
358 QByteArray sanitized;
359
360 const QString tokenString = QString::fromLatin1(token);
361 if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe) ||
362 token.size() != CSRFProtectionPrivate::tokenLength) {
363 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
364 } else {
365 sanitized = token;
366 }
367
368 return sanitized;
369}
370
375QByteArray CSRFProtectionPrivate::getToken(Context *c)
376{
377 QByteArray token;
378
379 if (!csrf) {
380 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
381 return token;
382 }
383
384 if (csrf->d_ptr->useSessions) {
385 token = Session::value(c, CSRFProtectionPrivate::sessionKey).toByteArray();
386 } else {
387 QByteArray cookieToken = c->req()->cookie(csrf->d_ptr->cookieName);
388 if (cookieToken.isEmpty()) {
389 return token;
390 }
391
392 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
393 if (token != cookieToken) {
394 c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
395 }
396 }
397
398 qCDebug(C_CSRFPROTECTION) << "Got token" << token << "from"
399 << (csrf->d_ptr->useSessions ? "sessions" : "cookie");
400
401 return token;
402}
403
408void CSRFProtectionPrivate::setToken(Context *c)
409{
410 if (!csrf) {
411 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
412 return;
413 }
414
415 if (csrf->d_ptr->useSessions) {
417 CSRFProtectionPrivate::sessionKey,
418 c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
419 } else {
420 QNetworkCookie cookie(csrf->d_ptr->cookieName,
421 c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
422 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
423 cookie.setDomain(csrf->d_ptr->cookieDomain);
424 }
425 if (csrf->d_ptr->cookieExpiration.count() == 0) {
426 cookie.setExpirationDate(QDateTime());
427 } else {
428#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
429 cookie.setExpirationDate(
430 QDateTime::currentDateTime().addDuration(csrf->d_ptr->cookieExpiration));
431#else
432 cookie.setExpirationDate(
433 QDateTime::currentDateTime().addSecs(csrf->d_ptr->cookieExpiration.count()));
434#endif
435 }
436 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
437 cookie.setPath(csrf->d_ptr->cookiePath);
438 cookie.setSecure(csrf->d_ptr->cookieSecure);
439 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
440 c->res()->setCookie(cookie);
441 c->res()->headers().pushHeader("Vary"_qba, "Cookie"_qba);
442 }
443
444 qCDebug(C_CSRFPROTECTION) << "Set token"
445 << c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
446 << "to" << (csrf->d_ptr->useSessions ? "session" : "cookie");
447}
448
454void CSRFProtectionPrivate::reject(Context *c,
455 const QString &logReason,
456 const QString &displayReason)
457{
458 c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, false);
459
460 if (!csrf) {
461 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
462 return;
463 }
464
465 if (C_CSRFPROTECTION().isWarningEnabled()) {
466 if (csrf->d_ptr->logFailedIp) {
467 qCWarning(C_CSRFPROTECTION).nospace().noquote()
468 << "Forbidden: (" << logReason << "): " << c->req()->path() << " ["
469 << c->req()->addressString() << "]";
470 } else {
471 qCWarning(C_CSRFPROTECTION).nospace().noquote()
472 << "Forbidden: (" << logReason << "): " << c->req()->path()
473 << " [IP logging disabled]";
474 }
475 }
476
477 c->res()->setStatus(Response::Forbidden);
478 c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
479
480 QString detachToCsrf = c->action()->attribute(u"CSRFDetachTo"_qs);
481 if (detachToCsrf.isEmpty()) {
482 detachToCsrf = csrf->d_ptr->defaultDetachTo;
483 }
484
485 Action *detachToAction = nullptr;
486
487 if (!detachToCsrf.isEmpty()) {
488 detachToAction = c->controller()->actionFor(detachToCsrf);
489 if (!detachToAction) {
490 detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
491 }
492 if (!detachToAction) {
493 qCWarning(C_CSRFPROTECTION)
494 << "Can not find action for" << detachToCsrf << "to detach to";
495 }
496 }
497
498 if (detachToAction) {
499 c->detach(detachToAction);
500 } else {
501 c->res()->setStatus(Response::Forbidden);
502 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
503 c->res()->setBody(csrf->d_ptr->genericErrorMessage);
504 c->res()->setContentType(csrf->d_ptr->genericContentType);
505 } else {
506 const QString title = c->translate("Cutelyst::CSRFProtection",
507 "403 Forbidden - CSRF protection check failed");
508 c->res()->setBody(QStringLiteral("<!DOCTYPE html>\n"
509 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
510 " <head>\n"
511 " <title>") +
512 title +
513 QStringLiteral("</title>\n"
514 " </head>\n"
515 " <body>\n"
516 " <h1>") +
517 title +
518 QStringLiteral("</h1>\n"
519 " <p>") +
520 displayReason +
521 QStringLiteral("</p>\n"
522 " </body>\n"
523 "</html>\n"));
524 c->res()->setContentType("text/html; charset=utf-8"_qba);
525 }
526 c->detach();
527 }
528}
529
530void CSRFProtectionPrivate::accept(Context *c)
531{
532 c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, true);
533 c->setStash(CSRFProtectionPrivate::stashKeyProcessingDone, true);
534}
535
540bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
541{
542 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
543 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
544
545 // to avoid timing attack
546 QByteArray::size_type diff = _t1.size() ^ _t2.size();
547 for (QByteArray::size_type i = 0; i < _t1.size() && i < _t2.size(); i++) {
548 diff |= _t1[i] ^ _t2[i];
549 }
550 return diff == 0;
551}
552
557void CSRFProtectionPrivate::beforeDispatch(Context *c)
558{
559 if (!csrf) {
560 CSRFProtectionPrivate::reject(
561 c,
562 u"CSRFProtection plugin not registered"_qs,
563 c->translate("Cutelyst::CSRFProtection",
564 "The CSRF protection plugin has not been registered."));
565 return;
566 }
567
568 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
569 if (!csrfToken.isNull()) {
570 c->setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
571 } else {
573 }
574
575 if (c->stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
576 return;
577 }
578
579 if (c->action()->attributes().contains(u"CSRFIgnore"_qs)) {
580 qCDebug(C_CSRFPROTECTION).noquote().nospace()
581 << "Action " << c->action()->className() << "::" << c->action()->reverse()
582 << " is ignored by the CSRF protection";
583 return;
584 }
585
586 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
587 if (!c->action()->attributes().contains(u"CSRFRequire"_qs)) {
588 qCDebug(C_CSRFPROTECTION)
589 << "Namespace" << c->action()->ns() << "is ignored by the CSRF protection";
590 return;
591 }
592 }
593
594 // only check the tokens if the method is not secure, e.g. POST
595 // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
596 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
597
598 bool ok = true;
599
600 // Suppose user visits http://example.com/
601 // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
602 // https://example.com/detonate-bomb/ and submits it via JavaScript.
603 //
604 // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
605 // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
606 // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
607 // For this reason, for https://example.com/ we need additional protection that treats
608 // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
609 // Referer header is missing for same-domain requests in only about 0.2% of cases or less,
610 // so we can use strict Referer checking.
611 if (c->req()->secure()) {
612 const auto referer = c->req()->headers().referer();
613
614 if (Q_UNLIKELY(referer.isEmpty())) {
615 CSRFProtectionPrivate::reject(
616 c,
617 u"Referer checking failed - no Referer"_qs,
618 c->translate("Cutelyst::CSRFProtection",
619 "Referer checking failed - no Referer."));
620 ok = false;
621 } else {
622 const QUrl refererUrl(QString::fromLatin1(referer));
623 if (Q_UNLIKELY(!refererUrl.isValid())) {
624 CSRFProtectionPrivate::reject(
625 c,
626 u"Referer checking failed - Referer is malformed"_qs,
627 c->translate("Cutelyst::CSRFProtection",
628 "Referer checking failed - Referer is malformed."));
629 ok = false;
630 } else {
631 if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String("https"))) {
632 CSRFProtectionPrivate::reject(
633 c,
634 u"Referer checking failed - Referer is insecure while "
635 "host is secure"_qs,
636 c->translate("Cutelyst::CSRFProtection",
637 "Referer checking failed - Referer is insecure while host "
638 "is secure."));
639 ok = false;
640 } else {
641 // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
642 // If not, obey the cookie rules (or those for the session cookie, if we
643 // use sessions
644 constexpr int httpPort = 80;
645 constexpr int httpsPort = 443;
646
647 const QUrl uri = c->req()->uri();
648 QString goodReferer;
649 if (!csrf->d_ptr->useSessions) {
650 goodReferer = csrf->d_ptr->cookieDomain;
651 }
652 if (goodReferer.isEmpty()) {
653 goodReferer = uri.host();
654 }
655 const int serverPort = uri.port(c->req()->secure() ? httpsPort : httpPort);
656 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
657 goodReferer += u':' + QString::number(serverPort);
658 }
659
660 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
661 goodHosts.append(goodReferer);
662
663 QString refererHost = refererUrl.host();
664 const int refererPort = refererUrl.port(
665 refererUrl.scheme().compare(u"https") == 0 ? httpsPort : httpPort);
666 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
667 refererHost += u':' + QString::number(refererPort);
668 }
669
670 bool refererCheck = false;
671 for (const auto &host : std::as_const(goodHosts)) {
672 if ((host.startsWith(u'.') &&
673 (refererHost.endsWith(host) || (refererHost == host.mid(1)))) ||
674 host == refererHost) {
675 refererCheck = true;
676 break;
677 }
678 }
679
680 if (Q_UNLIKELY(!refererCheck)) {
681 ok = false;
682 CSRFProtectionPrivate::reject(
683 c,
684 u"Referer checking failed - %1 does not match any "
685 "trusted origins"_qs.arg(QString::fromLatin1(referer)),
686 c->translate("Cutelyst::CSRFProtection",
687 "Referer checking failed - %1 does not match any "
688 "trusted origins.")
689 .arg(QString::fromLatin1(referer)));
690 }
691 }
692 }
693 }
694 }
695
696 if (Q_LIKELY(ok)) {
697 if (Q_UNLIKELY(csrfToken.isEmpty())) {
698 CSRFProtectionPrivate::reject(
699 c,
700 u"CSRF cookie not set"_qs,
701 c->translate("Cutelyst::CSRFProtection", "CSRF cookie not set."));
702 ok = false;
703 } else {
704
705 QByteArray requestCsrfToken;
706 // delete does not have body data
707 if (!c->req()->isDelete()) {
708 if (c->req()->contentType().compare("multipart/form-data") == 0) {
709 // everything is an upload, even our token
710 Upload *upload =
711 c->req()->upload(QString::fromLatin1(csrf->d_ptr->formInputName));
712 if (upload && upload->size() < 1024 /*FIXME*/) {
713 requestCsrfToken = upload->readAll();
714 }
715 } else
716 requestCsrfToken =
717 c->req()
718 ->bodyParam(QString::fromLatin1(csrf->d_ptr->formInputName))
719 .toLatin1();
720 }
721
722 if (requestCsrfToken.isEmpty()) {
723 requestCsrfToken = c->req()->header(csrf->d_ptr->headerName);
724 if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
725 qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
726 << "from HTTP header" << csrf->d_ptr->headerName;
727 } else {
728 qCDebug(C_CSRFPROTECTION)
729 << "Can not get token from HTTP header or form field.";
730 }
731 } else {
732 qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
733 << "from form field" << csrf->d_ptr->formInputName;
734 }
735
736 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
737
738 if (Q_UNLIKELY(
739 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
740 CSRFProtectionPrivate::reject(c,
741 u"CSRF token missing or incorrect"_qs,
742 c->translate("Cutelyst::CSRFProtection",
743 "CSRF token missing or incorrect."));
744 ok = false;
745 }
746 }
747 }
748
749 if (Q_LIKELY(ok)) {
750 CSRFProtectionPrivate::accept(c);
751 }
752 }
753
754 // Set the CSRF cookie even if it's already set, so we renew
755 // the expiry timer.
756
757 if (!c->stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
758 if (c->stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
759 return;
760 }
761 }
762
763 if (!c->stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
764 return;
765 }
766
767 CSRFProtectionPrivate::setToken(c);
768 c->setStash(CSRFProtectionPrivate::stashKeyCookieSet, true);
769}
770
771#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:43
Engine * engine() const noexcept
void beforeDispatch(Cutelyst::Context *c)
T plugin()
Returns the registered plugin that casts to the template type T.
Definition: application.h:102
void loadTranslations(const QString &filename, const QString &directory={}, const QString &prefix={}, const QString &suffix={})
void postForked(Cutelyst::Application *app)
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
static bool checkPassed(Context *c)
void setUseSessions(bool useSessions)
void setIgnoredNamespaces(const QStringList &namespaces)
void setDefaultDetachTo(const QString &actionNameOrPath)
void setGenericErrorContentType(const QByteArray &type)
void setHeaderName(const QByteArray &headerName)
void setErrorMsgStashKey(const QString &keyName)
void setFormFieldName(const QByteArray &fieldName)
void setCookieHttpOnly(bool httpOnly)
static QByteArray getToken(Context *c)
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:38
void stash(const QVariantHash &unite)
Definition: context.cpp:553
void detach(Action *action=nullptr)
Definition: context.cpp:338
Response * res() const noexcept
Definition: context.cpp:102
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
Definition: context.cpp:477
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:211
Dispatcher * dispatcher() const noexcept
Definition: context.cpp:138
Action * actionFor(QStringView name) const
Definition: controller.cpp:36
Action * getActionByPath(QStringView path) const
Definition: dispatcher.cpp:227
QVariantMap config(const QString &entity) const
user configuration for the application
Definition: engine.cpp:290
QByteArray referer() const noexcept
Definition: headers.cpp:310
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition: headers.cpp:458
QString addressString() const
Definition: request.cpp:39
bool isDelete() const noexcept
Definition: request.cpp:349
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:272
Upload * upload(QStringView name) const
Definition: request.h:607
Headers headers() const noexcept
Definition: request.cpp:307
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Definition: request.h:552
QByteArray header(QByteArrayView key) const noexcept
Definition: request.h:592
void setContentType(const QByteArray &type)
Definition: response.h:203
void setStatus(quint16 status) noexcept
Definition: response.cpp:72
void setBody(QIODevice *body)
Definition: response.cpp:102
Headers & headers() noexcept
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:197
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:158
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:173
Cutelyst Upload handles file upload request
Definition: upload.h:22
qint64 size() const override
Definition: upload.cpp:138
CUTELYST_LIBRARY std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition: utils.cpp:291
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:8