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 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
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