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