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
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
153{
154 Q_D(CSRFProtection);
155 if (!keyName.isEmpty()) {
156 d->errorMsgStashKey = keyName;
157 } else {
158 d->errorMsgStashKey = QStringLiteral("error_msg");
159 }
160}
161
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
181{
182 Q_D(CSRFProtection);
183 d->cookieName = cookieName;
184}
185
187{
188 Q_D(CSRFProtection);
189 d->headerName = headerName;
190}
191
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 |
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 if (C_CSRFPROTECTION().isWarningEnabled()) {
464 if (csrf->d_ptr->logFailedIp) {
465 qCWarning(C_CSRFPROTECTION).nospace().noquote()
466 << "Forbidden: (" << logReason << "): " << c->req()->path() << " [client "
467 << c->req()->addressString() << "]";
468 } else {
469 qCWarning(C_CSRFPROTECTION).nospace().noquote()
470 << "Forbidden: (" << logReason << "): " << c->req()->path();
471 }
472 }
473
474 c->res()->setStatus(Response::Forbidden);
475 c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
476
477 QString detachToCsrf = c->action()->attribute(QStringLiteral("CSRFDetachTo"));
478 if (detachToCsrf.isEmpty()) {
479 detachToCsrf = csrf->d_ptr->defaultDetachTo;
480 }
481
482 Action *detachToAction = nullptr;
483
484 if (!detachToCsrf.isEmpty()) {
485 detachToAction = c->controller()->actionFor(detachToCsrf);
486 if (!detachToAction) {
487 detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
488 }
489 if (!detachToAction) {
490 qCWarning(C_CSRFPROTECTION,
491 "Can not find action for \"%s\" to detach to.",
492 qPrintable(detachToCsrf));
493 }
494 }
495
496 if (detachToAction) {
497 c->detach(detachToAction);
498 } else {
499 c->res()->setStatus(403);
500 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
501 c->res()->setBody(csrf->d_ptr->genericErrorMessage);
502 c->res()->setContentType(csrf->d_ptr->genericContentType);
503 } else {
504 const QString title = c->translate("Cutelyst::CSRFProtection",
505 "403 Forbidden - CSRF protection check failed");
506 c->res()->setBody(QStringLiteral("<!DOCTYPE html>\n"
507 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
508 " <head>\n"
509 " <title>") +
510 title +
511 QStringLiteral("</title>\n"
512 " </head>\n"
513 " <body>\n"
514 " <h1>") +
515 title +
516 QStringLiteral("</h1>\n"
517 " <p>") +
518 displayReason +
519 QStringLiteral("</p>\n"
520 " </body>\n"
521 "</html>\n"));
522 c->res()->setContentType(QStringLiteral("text/html; charset=utf-8"));
523 }
524 c->detach();
525 }
526}
527
528void CSRFProtectionPrivate::accept(Context *c)
529{
530 c->setStash(CONTEXT_CSRF_CHECK_PASSED, true);
531 c->setStash(CONTEXT_CSRF_PROCESSING_DONE, true);
532}
533
538bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
539{
540 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
541 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
542
543 // to avoid timing attack
544 int diff = _t1.size() ^ _t2.size();
545 for (int i = 0; i < _t1.size() && i < _t2.size(); i++) {
546 diff |= _t1[i] ^ _t2[i];
547 }
548 return diff == 0;
549}
550
555void CSRFProtectionPrivate::beforeDispatch(Context *c)
556{
557 if (!csrf) {
558 CSRFProtectionPrivate::reject(
559 c,
560 QStringLiteral("CSRFProtection plugin not registered"),
561 c->translate("Cutelyst::CSRFProtection",
562 "The CSRF protection plugin has not been registered."));
563 return;
564 }
565
566 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
567 if (!csrfToken.isNull()) {
568 c->setStash(CONTEXT_CSRF_COOKIE, csrfToken);
569 } else {
571 }
572
573 if (c->stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
574 return;
575 }
576
577 if (c->action()->attributes().contains(QStringLiteral("CSRFIgnore"))) {
578 qCDebug(C_CSRFPROTECTION,
579 "Action \"%s::%s\" is ignored by the CSRF protection.",
580 qPrintable(c->action()->className()),
581 qPrintable(c->action()->reverse()));
582 return;
583 }
584
585 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
586 if (!c->action()->attributes().contains(QStringLiteral("CSRFRequire"))) {
587 qCDebug(C_CSRFPROTECTION,
588 "Namespace \"%s\" is ignored by the CSRF protection.",
589 qPrintable(c->action()->ns()));
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 QString referer = c->req()->headers().referer();
613
614 if (Q_UNLIKELY(referer.isEmpty())) {
615 CSRFProtectionPrivate::reject(
616 c,
617 QStringLiteral("Referer checking failed - no Referer"),
618 c->translate("Cutelyst::CSRFProtection",
619 "Referer checking failed - no Referer."));
620 ok = false;
621 } else {
622 const QUrl refererUrl(referer);
623 if (Q_UNLIKELY(!refererUrl.isValid())) {
624 CSRFProtectionPrivate::reject(
625 c,
626 QStringLiteral("Referer checking failed - Referer is malformed"),
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 QStringLiteral("Referer checking failed - Referer is insecure while "
635 "host is secure"),
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 const QUrl uri = c->req()->uri();
645 QString goodReferer;
646 if (!csrf->d_ptr->useSessions) {
647 goodReferer = csrf->d_ptr->cookieDomain;
648 }
649 if (goodReferer.isEmpty()) {
650 goodReferer = uri.host();
651 }
652 const int serverPort = uri.port(c->req()->secure() ? 443 : 80);
653 if ((serverPort != 80) && (serverPort != 443)) {
654 goodReferer += u':' + QString::number(serverPort);
655 }
656
657 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
658 goodHosts.append(goodReferer);
659
660 QString refererHost = refererUrl.host();
661 const int refererPort =
662 refererUrl.port(refererUrl.scheme().compare(u"https") == 0 ? 443 : 80);
663 if ((refererPort != 80) && (refererPort != 443)) {
664 refererHost += u':' + QString::number(refererPort);
665 }
666
667 bool refererCheck = false;
668 for (int i = 0; i < goodHosts.size(); ++i) {
669 const QString host = goodHosts.at(i);
670 if ((host.startsWith(u'.') &&
671 (refererHost.endsWith(host) || (refererHost == host.mid(1)))) ||
672 host == refererHost) {
673 refererCheck = true;
674 break;
675 }
676 }
677
678 if (Q_UNLIKELY(!refererCheck)) {
679 ok = false;
680 CSRFProtectionPrivate::reject(
681 c,
682 QStringLiteral("Referer checking failed - %1 does not match any "
683 "trusted origins")
684 .arg(referer),
685 c->translate("Cutelyst::CSRFProtection",
686 "Referer checking failed - %1 does not match any "
687 "trusted origins.")
688 .arg(referer));
689 }
690 }
691 }
692 }
693 }
694
695 if (Q_LIKELY(ok)) {
696 if (Q_UNLIKELY(csrfToken.isEmpty())) {
697 CSRFProtectionPrivate::reject(
698 c,
699 QStringLiteral("CSRF cookie not set"),
700 c->translate("Cutelyst::CSRFProtection", "CSRF cookie not set."));
701 ok = false;
702 } else {
703
704 QByteArray requestCsrfToken;
705 // delete does not have body data
706 if (!c->req()->isDelete()) {
707 if (c->req()->contentType().compare(u"multipart/form-data") == 0) {
708 // everything is an upload, even our token
709 Upload *upload = c->req()->upload(csrf->d_ptr->formInputName);
710 if (upload && upload->size() < 1024 /*FIXME*/) {
711 requestCsrfToken = upload->readAll();
712 }
713 } else
714 requestCsrfToken =
715 c->req()->bodyParam(csrf->d_ptr->formInputName).toLatin1();
716 }
717
718 if (requestCsrfToken.isEmpty()) {
719 requestCsrfToken = c->req()->header(csrf->d_ptr->headerName).toLatin1();
720 if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
721 qCDebug(C_CSRFPROTECTION,
722 "Got token \"%s\" from HTTP header %s.",
723 requestCsrfToken.constData(),
724 qPrintable(csrf->d_ptr->headerName));
725 } else {
726 qCDebug(C_CSRFPROTECTION,
727 "Can not get token from HTTP header or form field.");
728 }
729 } else {
730 qCDebug(C_CSRFPROTECTION,
731 "Got token \"%s\" from form field %s.",
732 requestCsrfToken.constData(),
733 qPrintable(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 QStringLiteral("CSRF token missing or incorrect"),
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(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
758 if (c->stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
759 return;
760 }
761 }
762
763 if (!c->stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
764 return;
765 }
766
767 CSRFProtectionPrivate::setToken(c);
768 c->setStash(CONTEXT_CSRF_COOKIE_SET, 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
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
Headers & headers() noexcept
Definition response.cpp:339
void setStatus(quint16 status) noexcept
Definition response.cpp:70
void setBody(QIODevice *body)
Definition response.cpp:100
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
QByteArray & append(char ch)
char at(int i) const const
const char * constData() const const
int indexOf(char ch, int from) const const
bool isEmpty() const const
bool isNull() const const
QByteArray left(int len) const const
QByteArray mid(int pos, int len) const const
void reserve(int size)
void resize(int size)
int size() const const
QDateTime currentDateTime()
QByteArray readAll()
void append(const T &value)
const T & at(int i) const const
int size() const const
bool contains(const Key &key, const T &value) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
int compare(const QString &other, Qt::CaseSensitivity cs) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString fromLatin1(const char *str, int size)
bool isEmpty() const const
QString mid(int position, int n) const const
QString number(int n, int base)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
CaseInsensitive
SkipEmptyParts
QString host(ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QUuid createUuid()
QByteArray toByteArray() const const