cutelyst 4.3.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
59CSRFProtection::CSRFProtection(Application *parent, const QVariantMap &defaultConfig)
60 : Plugin(parent)
61 , d_ptr(new CSRFProtectionPrivate)
62{
63 Q_D(CSRFProtection);
64 d->defaultConfig = defaultConfig;
65}
66
68
70{
71 Q_D(CSRFProtection);
72
73 app->loadTranslations(u"plugin_csrfprotection"_qs);
74
75 const QVariantMap config = app->engine()->config(u"Cutelyst_CSRFProtection_Plugin"_qs);
76
77 bool cookieExpirationOk = false;
78 const QString cookieExpireStr =
79 config
80 .value(u"cookie_expiration"_qs,
81 config.value(
82 u"cookie_age"_qs,
83 d->defaultConfig.value(
84 u"cookie_expiration"_qs,
85 static_cast<qint64>(std::chrono::duration_cast<std::chrono::seconds>(
86 CSRFProtectionPrivate::cookieDefaultExpiration)
87 .count()))))
88 .toString();
89 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
90 Utils::durationFromString(cookieExpireStr, &cookieExpirationOk));
91 if (!cookieExpirationOk) {
92 qCWarning(C_CSRFPROTECTION).nospace() << "Invalid value set for cookie_expiration. "
93 "Using default value "
94#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
95 << CSRFProtectionPrivate::cookieDefaultExpiration;
96#else
97 << "1 year";
98#endif
99 d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
100 }
101
102 d->cookieDomain =
103 config.value(u"cookie_domain"_qs, d->defaultConfig.value(u"cookie_domain"_qs)).toString();
104 if (d->cookieName.isEmpty()) {
105 d->cookieName = "csrftoken";
106 }
107 d->cookiePath = u"/"_qs;
108
109 const QString _sameSite =
110 config
111 .value(u"cookie_same_site"_qs,
112 d->defaultConfig.value(u"cookie_same_site"_qs, u"strict"_qs))
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"_qs, d->defaultConfig.value(u"cookie_secure"_qs, 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"_qs, d->defaultConfig.value(u"trusted_origins"_qs))
147 .toString()
149 if (d->formInputName.isEmpty()) {
150 d->formInputName = "csrfprotectiontoken";
151 }
152 d->logFailedIp =
153 config.value(u"log_failed_ip"_qs, d->defaultConfig.value(u"log_failed_ip"_qs, false))
154 .toBool();
155 if (d->errorMsgStashKey.isEmpty()) {
156 d->errorMsgStashKey = u"error_msg"_qs;
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 Q_D(CSRFProtection);
183 d->errorMsgStashKey = keyName;
184}
185
187{
188 Q_D(CSRFProtection);
189 d->ignoredNamespaces = namespaces;
190}
191
192void CSRFProtection::setUseSessions(bool useSessions)
193{
194 Q_D(CSRFProtection);
195 d->useSessions = useSessions;
196}
197
199{
200 Q_D(CSRFProtection);
201 d->cookieHttpOnly = httpOnly;
202}
203
205{
206 Q_D(CSRFProtection);
207 d->cookieName = cookieName;
208}
209
211{
212 Q_D(CSRFProtection);
213 d->headerName = headerName;
214}
215
217{
218 Q_D(CSRFProtection);
219 d->genericErrorMessage = message;
220}
221
223{
224 Q_D(CSRFProtection);
225 d->genericContentType = type;
226}
227
229{
230 QByteArray token;
231
232 const QByteArray contextCookie = c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
233 QByteArray secret;
234 if (contextCookie.isEmpty()) {
235 secret = CSRFProtectionPrivate::getNewCsrfString();
236 token = CSRFProtectionPrivate::saltCipherSecret(secret);
237 c->setStash(CSRFProtectionPrivate::stashKeyCookie, token);
238 } else {
239 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
240 token = CSRFProtectionPrivate::saltCipherSecret(secret);
241 }
242
243 c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
244
245 return token;
246}
247
249{
250 QString form;
251
252 if (!csrf) {
253 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
254 return form;
255 }
256
257 form = QStringLiteral("<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
258 .arg(QString::fromLatin1(csrf->d_ptr->formInputName),
260
261 return form;
262}
263
265{
266 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
267 return true;
268 } else {
269 return c->stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
270 }
271}
272
273// void CSRFProtection::rotateToken(Context *c)
274//{
275// c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
276// c->setStash(QString CSRFProtectionPrivate::stashKeyCookie,
277// CSRFProtectionPrivate::getNewCsrfToken());
278// c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
279// }
280
285QByteArray CSRFProtectionPrivate::getNewCsrfString()
286{
287 QByteArray csrfString;
288
289 while (csrfString.size() < CSRFProtectionPrivate::secretLength) {
290 csrfString.append(QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding |
292 }
293
294 csrfString.resize(CSRFProtectionPrivate::secretLength);
295
296 return csrfString;
297}
298
304QByteArray CSRFProtectionPrivate::saltCipherSecret(const QByteArray &secret)
305{
306 QByteArray salted;
307 salted.reserve(CSRFProtectionPrivate::tokenLength);
308
309 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
310 std::vector<std::pair<int, int>> pairs;
311 pairs.reserve(std::min(secret.size(), salt.size()));
312 for (int i = 0; i < std::min(secret.size(), salt.size()); ++i) {
313 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.at(i)),
314 CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
315 }
316
317 QByteArray cipher;
318 cipher.reserve(CSRFProtectionPrivate::secretLength);
319 for (const auto &p : std::as_const(pairs)) {
320 cipher.append(
321 CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
322 CSRFProtectionPrivate::allowedChars.size()]);
323 }
324
325 salted = salt + cipher;
326
327 return salted;
328}
329
336QByteArray CSRFProtectionPrivate::unsaltCipherToken(const QByteArray &token)
337{
338 QByteArray secret;
339 secret.reserve(CSRFProtectionPrivate::secretLength);
340
341 const QByteArray salt = token.left(CSRFProtectionPrivate::secretLength);
342 const QByteArray _token = token.mid(CSRFProtectionPrivate::secretLength);
343
344 std::vector<std::pair<int, int>> pairs;
345 pairs.reserve(std::min(salt.size(), _token.size()));
346 for (int i = 0; i < std::min(salt.size(), _token.size()); ++i) {
347 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.at(i)),
348 CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
349 }
350
351 for (const auto &p : std::as_const(pairs)) {
352 QByteArray::size_type idx = p.first - p.second;
353 if (idx < 0) {
354 idx = CSRFProtectionPrivate::allowedChars.size() + idx;
355 }
356 secret.append(CSRFProtectionPrivate::allowedChars.at(idx));
357 }
358
359 return secret;
360}
361
367QByteArray CSRFProtectionPrivate::getNewCsrfToken()
368{
369 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
370}
371
377QByteArray CSRFProtectionPrivate::sanitizeToken(const QByteArray &token)
378{
379 QByteArray sanitized;
380
381 const QString tokenString = QString::fromLatin1(token);
382 if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe) ||
383 token.size() != CSRFProtectionPrivate::tokenLength) {
384 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
385 } else {
386 sanitized = token;
387 }
388
389 return sanitized;
390}
391
396QByteArray CSRFProtectionPrivate::getToken(Context *c)
397{
398 QByteArray token;
399
400 if (!csrf) {
401 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
402 return token;
403 }
404
405 if (csrf->d_ptr->useSessions) {
406 token = Session::value(c, CSRFProtectionPrivate::sessionKey).toByteArray();
407 } else {
408 QByteArray cookieToken = c->req()->cookie(csrf->d_ptr->cookieName);
409 if (cookieToken.isEmpty()) {
410 return token;
411 }
412
413 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
414 if (token != cookieToken) {
415 c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
416 }
417 }
418
419 qCDebug(C_CSRFPROTECTION) << "Got token" << token << "from"
420 << (csrf->d_ptr->useSessions ? "sessions" : "cookie");
421
422 return token;
423}
424
429void CSRFProtectionPrivate::setToken(Context *c)
430{
431 if (!csrf) {
432 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
433 return;
434 }
435
436 if (csrf->d_ptr->useSessions) {
438 CSRFProtectionPrivate::sessionKey,
439 c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
440 } else {
441 QNetworkCookie cookie(csrf->d_ptr->cookieName,
442 c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
443 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
444 cookie.setDomain(csrf->d_ptr->cookieDomain);
445 }
446 if (csrf->d_ptr->cookieExpiration.count() == 0) {
447 cookie.setExpirationDate(QDateTime());
448 } else {
449#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
450 cookie.setExpirationDate(
451 QDateTime::currentDateTime().addDuration(csrf->d_ptr->cookieExpiration));
452#else
453 cookie.setExpirationDate(
454 QDateTime::currentDateTime().addSecs(csrf->d_ptr->cookieExpiration.count()));
455#endif
456 }
457 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
458 cookie.setPath(csrf->d_ptr->cookiePath);
459 cookie.setSecure(csrf->d_ptr->cookieSecure);
460 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
461 c->res()->setCookie(cookie);
462 c->res()->headers().pushHeader("Vary"_qba, "Cookie"_qba);
463 }
464
465 qCDebug(C_CSRFPROTECTION) << "Set token"
466 << c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
467 << "to" << (csrf->d_ptr->useSessions ? "session" : "cookie");
468}
469
475void CSRFProtectionPrivate::reject(Context *c,
476 const QString &logReason,
477 const QString &displayReason)
478{
479 c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, false);
480
481 if (!csrf) {
482 qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
483 return;
484 }
485
486 if (C_CSRFPROTECTION().isWarningEnabled()) {
487 if (csrf->d_ptr->logFailedIp) {
488 qCWarning(C_CSRFPROTECTION).nospace().noquote()
489 << "Forbidden: (" << logReason << "): " << c->req()->path() << " ["
490 << c->req()->addressString() << "]";
491 } else {
492 qCWarning(C_CSRFPROTECTION).nospace().noquote()
493 << "Forbidden: (" << logReason << "): " << c->req()->path()
494 << " [IP logging disabled]";
495 }
496 }
497
498 c->res()->setStatus(Response::Forbidden);
499 c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
500
501 QString detachToCsrf = c->action()->attribute(u"CSRFDetachTo"_qs);
502 if (detachToCsrf.isEmpty()) {
503 detachToCsrf = csrf->d_ptr->defaultDetachTo;
504 }
505
506 Action *detachToAction = nullptr;
507
508 if (!detachToCsrf.isEmpty()) {
509 detachToAction = c->controller()->actionFor(detachToCsrf);
510 if (!detachToAction) {
511 detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
512 }
513 if (!detachToAction) {
514 qCWarning(C_CSRFPROTECTION)
515 << "Can not find action for" << detachToCsrf << "to detach to";
516 }
517 }
518
519 if (detachToAction) {
520 c->detach(detachToAction);
521 } else {
522 c->res()->setStatus(Response::Forbidden);
523 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
524 c->res()->setBody(csrf->d_ptr->genericErrorMessage);
525 c->res()->setContentType(csrf->d_ptr->genericContentType);
526 } else {
527 //% "403 Forbidden - CSRF protection check failed"
528 const QString title = c->qtTrId("cutelyst-csrf-generic-error-page-title");
529 c->res()->setBody(QStringLiteral("<!DOCTYPE html>\n"
530 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
531 " <head>\n"
532 " <title>") +
533 title +
534 QStringLiteral("</title>\n"
535 " </head>\n"
536 " <body>\n"
537 " <h1>") +
538 title +
539 QStringLiteral("</h1>\n"
540 " <p>") +
541 displayReason +
542 QStringLiteral("</p>\n"
543 " </body>\n"
544 "</html>\n"));
545 c->res()->setContentType("text/html; charset=utf-8"_qba);
546 }
547 c->finalize();
548 }
549}
550
551void CSRFProtectionPrivate::accept(Context *c)
552{
553 c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, true);
554 c->setStash(CSRFProtectionPrivate::stashKeyProcessingDone, true);
555}
556
561bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
562{
563 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
564 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
565
566 // to avoid timing attack
567 QByteArray::size_type diff = _t1.size() ^ _t2.size();
568 for (QByteArray::size_type i = 0; i < _t1.size() && i < _t2.size(); i++) {
569 diff |= _t1[i] ^ _t2[i];
570 }
571 return diff == 0;
572}
573
578void CSRFProtectionPrivate::beforeDispatch(Context *c)
579{
580 if (!csrf) {
581 CSRFProtectionPrivate::reject(c,
582 u"CSRFProtection plugin not registered"_qs,
583 //% "The CSRF protection plugin has not been registered."
584 c->qtTrId("cutelyst-csrf-reject-not-registered"));
585 return;
586 }
587
588 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
589 if (!csrfToken.isNull()) {
590 c->setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
591 } else {
593 }
594
595 if (c->stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
596 return;
597 }
598
599 if (c->action()->attributes().contains(u"CSRFIgnore"_qs)) {
600 qCDebug(C_CSRFPROTECTION).noquote().nospace()
601 << "Action " << c->action()->className() << "::" << c->action()->reverse()
602 << " is ignored by the CSRF protection";
603 return;
604 }
605
606 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
607 if (!c->action()->attributes().contains(u"CSRFRequire"_qs)) {
608 qCDebug(C_CSRFPROTECTION)
609 << "Namespace" << c->action()->ns() << "is ignored by the CSRF protection";
610 return;
611 }
612 }
613
614 // only check the tokens if the method is not secure, e.g. POST
615 // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
616 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
617
618 bool ok = true;
619
620 // Suppose user visits http://example.com/
621 // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
622 // https://example.com/detonate-bomb/ and submits it via JavaScript.
623 //
624 // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
625 // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
626 // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
627 // For this reason, for https://example.com/ we need additional protection that treats
628 // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
629 // Referer header is missing for same-domain requests in only about 0.2% of cases or less,
630 // so we can use strict Referer checking.
631 if (c->req()->secure()) {
632 const auto referer = c->req()->headers().referer();
633
634 if (Q_UNLIKELY(referer.isEmpty())) {
635 CSRFProtectionPrivate::reject(c,
636 u"Referer checking failed - no Referer"_qs,
637 //% "Referrer checking failed - no Referrer."
638 c->qtTrId("cutelyst-csrf-reject-no-referer"));
639 ok = false;
640 } else {
641 const QUrl refererUrl(QString::fromLatin1(referer));
642 if (Q_UNLIKELY(!refererUrl.isValid())) {
643 CSRFProtectionPrivate::reject(
644 c,
645 u"Referer checking failed - Referer is malformed"_qs,
646 //% "Referrer checking failed - Referrer is malformed."
647 c->qtTrId("cutelyst-csrf-reject-referer-malformed"));
648 ok = false;
649 } else {
650 if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String("https"))) {
651 CSRFProtectionPrivate::reject(
652 c,
653 u"Referer checking failed - Referer is insecure while "
654 "host is secure"_qs,
655 //% "Referrer checking failed - Referrer is insecure while host "
656 //% "is secure."
657 c->qtTrId("cutelyst-csrf-reject-refer-insecure"));
658 ok = false;
659 } else {
660 // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
661 // If not, obey the cookie rules (or those for the session cookie, if we
662 // use sessions
663 constexpr int httpPort = 80;
664 constexpr int httpsPort = 443;
665
666 const QUrl uri = c->req()->uri();
667 QString goodReferer;
668 if (!csrf->d_ptr->useSessions) {
669 goodReferer = csrf->d_ptr->cookieDomain;
670 }
671 if (goodReferer.isEmpty()) {
672 goodReferer = uri.host();
673 }
674 const int serverPort = uri.port(c->req()->secure() ? httpsPort : httpPort);
675 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
676 goodReferer += u':' + QString::number(serverPort);
677 }
678
679 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
680 goodHosts.append(goodReferer);
681
682 QString refererHost = refererUrl.host();
683 const int refererPort = refererUrl.port(
684 refererUrl.scheme().compare(u"https") == 0 ? httpsPort : httpPort);
685 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
686 refererHost += u':' + QString::number(refererPort);
687 }
688
689 bool refererCheck = false;
690 for (const auto &host : std::as_const(goodHosts)) {
691 if ((host.startsWith(u'.') &&
692 (refererHost.endsWith(host) || (refererHost == host.mid(1)))) ||
693 host == refererHost) {
694 refererCheck = true;
695 break;
696 }
697 }
698
699 if (Q_UNLIKELY(!refererCheck)) {
700 ok = false;
701 CSRFProtectionPrivate::reject(
702 c,
703 u"Referer checking failed - %1 does not match any "
704 "trusted origins"_qs.arg(QString::fromLatin1(referer)),
705 //% "Referrer checking failed - %1 does not match any "
706 //% "trusted origin."
707 c->qtTrId("cutelyst-csrf-reject-referer-no-trust")
708 .arg(QString::fromLatin1(referer)));
709 }
710 }
711 }
712 }
713 }
714
715 if (Q_LIKELY(ok)) {
716 if (Q_UNLIKELY(csrfToken.isEmpty())) {
717 CSRFProtectionPrivate::reject(c,
718 u"CSRF cookie not set"_qs,
719 //% "CSRF cookie not set."
720 c->qtTrId("cutelyst-csrf-reject-no-cookie"));
721 ok = false;
722 } else {
723
724 QByteArray requestCsrfToken;
725 // delete does not have body data
726 if (!c->req()->isDelete()) {
727 if (c->req()->contentType().compare("multipart/form-data") == 0) {
728 // everything is an upload, even our token
729 Upload *upload =
730 c->req()->upload(QString::fromLatin1(csrf->d_ptr->formInputName));
731 if (upload && upload->size() < 1024 /*FIXME*/) {
732 requestCsrfToken = upload->readAll();
733 }
734 } else
735 requestCsrfToken =
736 c->req()
737 ->bodyParam(QString::fromLatin1(csrf->d_ptr->formInputName))
738 .toLatin1();
739 }
740
741 if (requestCsrfToken.isEmpty()) {
742 requestCsrfToken = c->req()->header(csrf->d_ptr->headerName);
743 if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
744 qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
745 << "from HTTP header" << csrf->d_ptr->headerName;
746 } else {
747 qCDebug(C_CSRFPROTECTION)
748 << "Can not get token from HTTP header or form field.";
749 }
750 } else {
751 qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
752 << "from form field" << csrf->d_ptr->formInputName;
753 }
754
755 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
756
757 if (Q_UNLIKELY(
758 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
759 CSRFProtectionPrivate::reject(c,
760 u"CSRF token missing or incorrect"_qs,
761 //% "CSRF token missing or incorrect."
762 c->qtTrId("cutelyst-csrf-reject-token-missin"));
763 ok = false;
764 }
765 }
766 }
767
768 if (Q_LIKELY(ok)) {
769 CSRFProtectionPrivate::accept(c);
770 }
771 }
772
773 // Set the CSRF cookie even if it's already set, so we renew
774 // the expiry timer.
775
776 if (!c->stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
777 if (c->stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
778 return;
779 }
780 }
781
782 if (!c->stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
783 return;
784 }
785
786 CSRFProtectionPrivate::setToken(c);
787 c->setStash(CSRFProtectionPrivate::stashKeyCookieSet, true);
788}
789
790#include "moc_csrfprotection.cpp"
This class represents a Cutelyst Action.
Definition action.h:36
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 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:339
Response * res() const noexcept
Definition context.cpp:103
void setStash(const QString &key, const QVariant &value)
Definition context.cpp:212
Request * req
Definition context.h:66
Controller * controller
Definition context.h:75
QString qtTrId(const char *id, int n=-1) const
Definition context.h:656
Action * action
Definition context.h:47
Dispatcher * dispatcher() const noexcept
Definition context.cpp:139
Action * actionFor(QStringView name) const
Action * getActionByPath(QStringView path) const
QVariantMap config(const QString &entity) const
Definition engine.cpp:263
QByteArray referer() const noexcept
Definition headers.cpp:310
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition headers.cpp:458
Base class for Cutelyst Plugins.
Definition plugin.h:25
QString addressString() const
Definition request.cpp:39
bool isDelete() const noexcept
Definition request.cpp:354
QByteArray cookie(QByteArrayView name) const
Definition request.cpp:277
Upload * upload(QStringView name) const
Definition request.h:626
Headers headers() const noexcept
Definition request.cpp:312
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Definition request.h:571
QByteArray header(QByteArrayView key) const noexcept
Definition request.h:611
void setContentType(const QByteArray &type)
Definition response.h:238
void setStatus(quint16 status) noexcept
Definition response.cpp:72
void setBody(QIODevice *body)
Definition response.cpp:103
Headers & headers() noexcept
void setCookie(const QNetworkCookie &cookie)
Definition response.cpp:212
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition session.cpp:168
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition session.cpp:183
Cutelyst Upload handles file upload requests.
Definition upload.h:26
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.
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