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 
45 Q_LOGGING_CATEGORY(C_CSRFPROTECTION, "cutelyst.plugin.csrfprotection", QtWarningMsg)
46 
47 using namespace Cutelyst;
48 
49 static thread_local CSRFProtection *csrf = nullptr;
50 const QRegularExpression CSRFProtectionPrivate::sanitizeRe =
51  QRegularExpression(QStringLiteral("[^a-zA-Z0-9\\-_]"));
52 // Assume that anything not defined as 'safe' by RFC7231 needs protection
53 const 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 
136 void 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 
168 void 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 
180 void CSRFProtection::setCookieName(const QString &cookieName)
181 {
182  Q_D(CSRFProtection);
183  d->cookieName = cookieName;
184 }
185 
186 void CSRFProtection::setHeaderName(const QString &headerName)
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 
259 QByteArray 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 
278 QByteArray 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 
309 QByteArray 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 
341 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
342 {
343  return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
344 }
345 
351 QByteArray 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 
371 QByteArray 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 
407 void 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 
452 void 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 
524 void CSRFProtectionPrivate::accept(Context *c)
525 {
526  c->setStash(CONTEXT_CSRF_CHECK_PASSED, true);
527  c->setStash(CONTEXT_CSRF_PROCESSING_DONE, true);
528 }
529 
534 bool 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 
551 void 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"
ParamsMultiMap attributes() const noexcept
Definition: action.cpp:68
void pushHeader(const QString &field, const QString &value)
Definition: headers.cpp:406
QByteArray toByteArray() const const
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:232
void setHeaderName(const QString &headerName)
void postForked(Cutelyst::Application *app)
void setContentType(const QString &type)
Definition: response.h:220
void reserve(qsizetype size)
char at(qsizetype i) const const
Headers & headers() noexcept
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool isDelete() const noexcept
Definition: request.cpp:352
Response * res() const noexcept
Definition: context.cpp:102
const_reference at(qsizetype i) const const
bool isNull() const const
QString host(ComponentFormattingOptions options) const const
bool isEmpty() const const
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:217
void detach(Action *action=nullptr)
Definition: context.cpp:345
void loadTranslations(const QString &filename, const QString &directory=QString(), const QString &prefix=QString(), const QString &suffix=QString())
Action * actionFor(const QString &name) const
Definition: controller.cpp:36
int port(int defaultPort) const const
T plugin()
Returns the registered plugin that casts to the template type T.
Definition: application.h:107
void setGenericErrorContentTyp(const QString &type)
qsizetype size() const const
This class represents a Cutelyst Action.
Definition: action.h:34
void setIgnoredNamespaces(const QStringList &namespaces)
Cutelyst Upload handles file upload request
Definition: upload.h:22
virtual bool setup(Application *app) override
The Cutelyst Context.
Definition: context.h:38
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
QString number(double n, char format, int precision)
virtual ~CSRFProtection() override
void setDomain(const QString &domain)
QString addressString() const
Definition: request.cpp:39
void stash(const QVariantHash &unite)
Definition: context.cpp:566
Upload * upload(const QString &name) const
Definition: request.h:596
Headers headers() const noexcept
Definition: request.cpp:310
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
CaseInsensitive
static bool checkPassed(Context *c)
QVariantMap config(const QString &entity) const
user configuration for the application
Definition: engine.cpp:290
bool isEmpty() const const
const char * constData() const const
QByteArray readAll()
QDateTime currentDateTime()
void setDefaultDetachTo(const QString &actionNameOrPath)
QString header(const QString &key) const
Definition: request.h:581
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
Definition: context.cpp:490
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:185
void setFormFieldName(const QString &fieldName)
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:7
QString reverse() const
Definition: component.cpp:45
QByteArray mid(qsizetype pos, qsizetype len) const const
void setCookieName(const QString &cookieName)
void setUseSessions(bool useSessions)
QByteArray & append(QByteArrayView data)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString ns() const noexcept
Definition: action.cpp:118
SkipEmptyParts
void beforeDispatch(Cutelyst::Context *c)
void setCookieHttpOnly(bool httpOnly)
void resize(qsizetype newSize, char c)
bool contains(const Key &key) const const
QString fromLatin1(QByteArrayView str)
void setGenericErrorMessage(const QString &message)
QByteArray left(qsizetype len) const const
QByteArray toLatin1() const const
QString mid(qsizetype position, qsizetype n) const const
void append(QList< T > &&value)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:170
void setErrorMsgStashKey(const QString &keyName)
QString cookie(const QString &name) const
Definition: request.cpp:274
The Cutelyst Application.
Definition: application.h:42
Engine * engine() const noexcept
CSRFProtection(Application *parent)
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Definition: request.h:541
void setBody(QIODevice *body)
Definition: response.cpp:100
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition: action.cpp:74
qsizetype size() const const
Action * getActionByPath(const QString &path) const
Definition: dispatcher.cpp:223
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
static QString getTokenFormField(Context *c)
QString arg(Args &&... args) const const
void setStatus(quint16 status) noexcept
Definition: response.cpp:70
static QByteArray getToken(Context *c)
The Cutelyst Cookie.
Definition: cookie.h:28
QUuid createUuid()
virtual qint64 size() const override
Definition: upload.cpp:137
QString referer() const
Definition: headers.cpp:306
QString className() const
Definition: action.cpp:86
Dispatcher * dispatcher() const noexcept
Definition: context.cpp:138