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