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(int size)
char at(int i) const const
Headers & headers() noexcept
bool isDelete() const noexcept
Definition: request.cpp:352
Response * res() const noexcept
Definition: context.cpp:102
const T & at(int i) const const
bool isNull() const const
QString host(QUrl::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)
int 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
void resize(int size)
virtual bool setup(Application *app) override
The Cutelyst Context.
Definition: context.h:38
int indexOf(char ch, int from) const const
QString number(int n, int base)
virtual ~CSRFProtection() override
void append(const T &value)
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
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
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray readAll()
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
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
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(int pos, int len) const const
void setCookieName(const QString &cookieName)
void setUseSessions(bool useSessions)
QByteArray & append(char ch)
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)
bool contains(const Key &key, const T &value) const const
void setGenericErrorMessage(const QString &message)
QByteArray left(int len) const const
QDateTime currentDateTime()
QByteArray toLatin1() const const
QString mid(int position, int n) const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:170
void setErrorMsgStashKey(const QString &keyName)
QString fromLatin1(const char *str, int size)
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
int size() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Action * getActionByPath(const QString &path) const
Definition: dispatcher.cpp:223
int compare(const QString &other, Qt::CaseSensitivity cs) const const
static QString getTokenFormField(Context *c)
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