cutelyst  4.3.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/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 <Cutelyst/utils.h>
20 #include <algorithm>
21 #include <utility>
22 #include <vector>
23 
24 #include <QLoggingCategory>
25 #include <QNetworkCookie>
26 #include <QUrl>
27 #include <QUuid>
28 
29 Q_LOGGING_CATEGORY(C_CSRFPROTECTION, "cutelyst.plugin.csrfprotection", QtWarningMsg)
30 
31 using namespace Cutelyst;
32 
33 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
34 static thread_local CSRFProtection *csrf = nullptr;
35 const QRegularExpression CSRFProtectionPrivate::sanitizeRe{u"[^a-zA-Z0-9\\-_]"_qs};
36 // Assume that anything not defined as 'safe' by RFC7231 needs protection
37 const QByteArrayList CSRFProtectionPrivate::secureMethods = QByteArrayList({
38  "GET",
39  "HEAD",
40  "OPTIONS",
41  "TRACE",
42 });
43 const QByteArray CSRFProtectionPrivate::allowedChars{
44  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"_qba};
45 const QString CSRFProtectionPrivate::sessionKey{u"_csrftoken"_qs};
46 const QString CSRFProtectionPrivate::stashKeyCookie{u"_c_csrfcookie"_qs};
47 const QString CSRFProtectionPrivate::stashKeyCookieUsed{u"_c_csrfcookieused"_qs};
48 const QString CSRFProtectionPrivate::stashKeyCookieNeedsReset{u"_c_csrfcookieneedsreset"_qs};
49 const QString CSRFProtectionPrivate::stashKeyCookieSet{u"_c_csrfcookieset"_qs};
50 const QString CSRFProtectionPrivate::stashKeyProcessingDone{u"_c_csrfprocessingdone"_qs};
51 const QString CSRFProtectionPrivate::stashKeyCheckPassed{u"_c_csrfcheckpassed"_qs};
52 
53 CSRFProtection::CSRFProtection(Application *parent)
54  : Plugin(parent)
55  , d_ptr(new CSRFProtectionPrivate)
56 {
57 }
58 
59 CSRFProtection::CSRFProtection(Application *parent, const QVariantMap &defaultConfig)
60  : Plugin(parent)
61  , d_ptr(new CSRFProtectionPrivate)
62 {
63  Q_D(CSRFProtection);
64  d->defaultConfig = defaultConfig;
65 }
66 
67 CSRFProtection::~CSRFProtection() = default;
68 
70 {
71  Q_D(CSRFProtection);
72 
73  app->loadTranslations(u"plugin_csrfprotection"_qs);
74 
75  const QVariantMap config = app->engine()->config(u"Cutelyst_CSRFProtection_Plugin"_qs);
76 
77  bool cookieExpirationOk = false;
78  const QString cookieExpireStr =
79  config
80  .value(u"cookie_expiration"_qs,
81  config.value(
82  u"cookie_age"_qs,
83  d->defaultConfig.value(
84  u"cookie_expiration"_qs,
85  static_cast<qint64>(std::chrono::duration_cast<std::chrono::seconds>(
86  CSRFProtectionPrivate::cookieDefaultExpiration)
87  .count()))))
88  .toString();
89  d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
90  Utils::durationFromString(cookieExpireStr, &cookieExpirationOk));
91  if (!cookieExpirationOk) {
92  qCWarning(C_CSRFPROTECTION).nospace() << "Invalid value set for cookie_expiration. "
93  "Using default value "
94 #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
95  << CSRFProtectionPrivate::cookieDefaultExpiration;
96 #else
97  << "1 year";
98 #endif
99  d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
100  }
101 
102  d->cookieDomain =
103  config.value(u"cookie_domain"_qs, d->defaultConfig.value(u"cookie_domain"_qs)).toString();
104  if (d->cookieName.isEmpty()) {
105  d->cookieName = "csrftoken";
106  }
107  d->cookiePath = u"/"_qs;
108 
109  const QString _sameSite =
110  config
111  .value(u"cookie_same_site"_qs,
112  d->defaultConfig.value(u"cookie_same_site"_qs, u"strict"_qs))
113  .toString();
114  if (_sameSite.compare(u"default", Qt::CaseInsensitive) == 0) {
115  d->cookieSameSite = QNetworkCookie::SameSite::Default;
116  } else if (_sameSite.compare(u"none", Qt::CaseInsensitive) == 0) {
117  d->cookieSameSite = QNetworkCookie::SameSite::None;
118  } else if (_sameSite.compare(u"lax", Qt::CaseInsensitive) == 0) {
119  d->cookieSameSite = QNetworkCookie::SameSite::Lax;
120  } else if (_sameSite.compare(u"strict", Qt::CaseInsensitive) == 0) {
121  d->cookieSameSite = QNetworkCookie::SameSite::Strict;
122  } else {
123  qCWarning(C_CSRFPROTECTION).nospace() << "Invalid value set for cookie_same_site. "
124  "Using default value "
125  << QNetworkCookie::SameSite::Strict;
126  d->cookieSameSite = QNetworkCookie::SameSite::Strict;
127  }
128 
129  d->cookieSecure =
130  config.value(u"cookie_secure"_qs, d->defaultConfig.value(u"cookie_secure"_qs, false))
131  .toBool();
132 
133  if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
134  qCWarning(C_CSRFPROTECTION)
135  << "cookie_same_site has been set to None but cookie_secure is "
136  "not set to true. Implicitely setting cookie_secure to true. "
137  "Please check your configuration.";
138  d->cookieSecure = true;
139  }
140 
141  if (d->headerName.isEmpty()) {
142  d->headerName = "X_CSRFTOKEN";
143  }
144 
145  d->trustedOrigins =
146  config.value(u"trusted_origins"_qs, d->defaultConfig.value(u"trusted_origins"_qs))
147  .toString()
148  .split(u',', Qt::SkipEmptyParts);
149  if (d->formInputName.isEmpty()) {
150  d->formInputName = "csrfprotectiontoken";
151  }
152  d->logFailedIp =
153  config.value(u"log_failed_ip"_qs, d->defaultConfig.value(u"log_failed_ip"_qs, false))
154  .toBool();
155  if (d->errorMsgStashKey.isEmpty()) {
156  d->errorMsgStashKey = u"error_msg"_qs;
157  }
158 
159  connect(app, &Application::postForked, this, [](Application *app) {
160  csrf = app->plugin<CSRFProtection *>();
161  });
162 
163  connect(app, &Application::beforeDispatch, this, [d](Context *c) { d->beforeDispatch(c); });
164 
165  return true;
166 }
167 
168 void CSRFProtection::setDefaultDetachTo(const QString &actionNameOrPath)
169 {
170  Q_D(CSRFProtection);
171  d->defaultDetachTo = actionNameOrPath;
172 }
173 
174 void CSRFProtection::setFormFieldName(const QByteArray &fieldName)
175 {
176  Q_D(CSRFProtection);
177  d->formInputName = fieldName;
178 }
179 
180 void CSRFProtection::setErrorMsgStashKey(const QString &keyName)
181 {
182  Q_D(CSRFProtection);
183  d->errorMsgStashKey = keyName;
184 }
185 
186 void CSRFProtection::setIgnoredNamespaces(const QStringList &namespaces)
187 {
188  Q_D(CSRFProtection);
189  d->ignoredNamespaces = namespaces;
190 }
191 
192 void CSRFProtection::setUseSessions(bool useSessions)
193 {
194  Q_D(CSRFProtection);
195  d->useSessions = useSessions;
196 }
197 
198 void CSRFProtection::setCookieHttpOnly(bool httpOnly)
199 {
200  Q_D(CSRFProtection);
201  d->cookieHttpOnly = httpOnly;
202 }
203 
204 void CSRFProtection::setCookieName(const QByteArray &cookieName)
205 {
206  Q_D(CSRFProtection);
207  d->cookieName = cookieName;
208 }
209 
210 void CSRFProtection::setHeaderName(const QByteArray &headerName)
211 {
212  Q_D(CSRFProtection);
213  d->headerName = headerName;
214 }
215 
216 void CSRFProtection::setGenericErrorMessage(const QString &message)
217 {
218  Q_D(CSRFProtection);
219  d->genericErrorMessage = message;
220 }
221 
222 void CSRFProtection::setGenericErrorContentType(const QByteArray &type)
223 {
224  Q_D(CSRFProtection);
225  d->genericContentType = type;
226 }
227 
228 QByteArray CSRFProtection::getToken(Context *c)
229 {
230  QByteArray token;
231 
232  const QByteArray contextCookie = c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
233  QByteArray secret;
234  if (contextCookie.isEmpty()) {
235  secret = CSRFProtectionPrivate::getNewCsrfString();
236  token = CSRFProtectionPrivate::saltCipherSecret(secret);
237  c->setStash(CSRFProtectionPrivate::stashKeyCookie, token);
238  } else {
239  secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
240  token = CSRFProtectionPrivate::saltCipherSecret(secret);
241  }
242 
243  c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
244 
245  return token;
246 }
247 
248 QString CSRFProtection::getTokenFormField(Context *c)
249 {
250  QString form;
251 
252  if (!csrf) {
253  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
254  return form;
255  }
256 
257  form = QStringLiteral("<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
258  .arg(QString::fromLatin1(csrf->d_ptr->formInputName),
259  QString::fromLatin1(CSRFProtection::getToken(c)));
260 
261  return form;
262 }
263 
264 bool CSRFProtection::checkPassed(Context *c)
265 {
266  if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
267  return true;
268  } else {
269  return c->stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
270  }
271 }
272 
273 // void CSRFProtection::rotateToken(Context *c)
274 //{
275 // c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
276 // c->setStash(QString CSRFProtectionPrivate::stashKeyCookie,
277 // CSRFProtectionPrivate::getNewCsrfToken());
278 // c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
279 // }
280 
285 QByteArray CSRFProtectionPrivate::getNewCsrfString()
286 {
287  QByteArray csrfString;
288 
289  while (csrfString.size() < CSRFProtectionPrivate::secretLength) {
290  csrfString.append(QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding |
292  }
293 
294  csrfString.resize(CSRFProtectionPrivate::secretLength);
295 
296  return csrfString;
297 }
298 
304 QByteArray CSRFProtectionPrivate::saltCipherSecret(const QByteArray &secret)
305 {
306  QByteArray salted;
307  salted.reserve(CSRFProtectionPrivate::tokenLength);
308 
309  const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
310  std::vector<std::pair<int, int>> pairs;
311  pairs.reserve(std::min(secret.size(), salt.size()));
312  for (int i = 0; i < std::min(secret.size(), salt.size()); ++i) {
313  pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.at(i)),
314  CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
315  }
316 
317  QByteArray cipher;
318  cipher.reserve(CSRFProtectionPrivate::secretLength);
319  for (const auto &p : std::as_const(pairs)) {
320  cipher.append(
321  CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
322  CSRFProtectionPrivate::allowedChars.size()]);
323  }
324 
325  salted = salt + cipher;
326 
327  return salted;
328 }
329 
336 QByteArray CSRFProtectionPrivate::unsaltCipherToken(const QByteArray &token)
337 {
338  QByteArray secret;
339  secret.reserve(CSRFProtectionPrivate::secretLength);
340 
341  const QByteArray salt = token.left(CSRFProtectionPrivate::secretLength);
342  const QByteArray _token = token.mid(CSRFProtectionPrivate::secretLength);
343 
344  std::vector<std::pair<int, int>> pairs;
345  pairs.reserve(std::min(salt.size(), _token.size()));
346  for (int i = 0; i < std::min(salt.size(), _token.size()); ++i) {
347  pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.at(i)),
348  CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
349  }
350 
351  for (const auto &p : std::as_const(pairs)) {
352  QByteArray::size_type idx = p.first - p.second;
353  if (idx < 0) {
354  idx = CSRFProtectionPrivate::allowedChars.size() + idx;
355  }
356  secret.append(CSRFProtectionPrivate::allowedChars.at(idx));
357  }
358 
359  return secret;
360 }
361 
367 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
368 {
369  return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
370 }
371 
377 QByteArray CSRFProtectionPrivate::sanitizeToken(const QByteArray &token)
378 {
379  QByteArray sanitized;
380 
381  const QString tokenString = QString::fromLatin1(token);
382  if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe) ||
383  token.size() != CSRFProtectionPrivate::tokenLength) {
384  sanitized = CSRFProtectionPrivate::getNewCsrfToken();
385  } else {
386  sanitized = token;
387  }
388 
389  return sanitized;
390 }
391 
396 QByteArray CSRFProtectionPrivate::getToken(Context *c)
397 {
398  QByteArray token;
399 
400  if (!csrf) {
401  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
402  return token;
403  }
404 
405  if (csrf->d_ptr->useSessions) {
406  token = Session::value(c, CSRFProtectionPrivate::sessionKey).toByteArray();
407  } else {
408  QByteArray cookieToken = c->req()->cookie(csrf->d_ptr->cookieName);
409  if (cookieToken.isEmpty()) {
410  return token;
411  }
412 
413  token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
414  if (token != cookieToken) {
415  c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
416  }
417  }
418 
419  qCDebug(C_CSRFPROTECTION) << "Got token" << token << "from"
420  << (csrf->d_ptr->useSessions ? "sessions" : "cookie");
421 
422  return token;
423 }
424 
429 void CSRFProtectionPrivate::setToken(Context *c)
430 {
431  if (!csrf) {
432  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
433  return;
434  }
435 
436  if (csrf->d_ptr->useSessions) {
438  CSRFProtectionPrivate::sessionKey,
439  c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
440  } else {
441  QNetworkCookie cookie(csrf->d_ptr->cookieName,
442  c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
443  if (!csrf->d_ptr->cookieDomain.isEmpty()) {
444  cookie.setDomain(csrf->d_ptr->cookieDomain);
445  }
446  if (csrf->d_ptr->cookieExpiration.count() == 0) {
447  cookie.setExpirationDate(QDateTime());
448  } else {
449 #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
450  cookie.setExpirationDate(
451  QDateTime::currentDateTime().addDuration(csrf->d_ptr->cookieExpiration));
452 #else
453  cookie.setExpirationDate(
454  QDateTime::currentDateTime().addSecs(csrf->d_ptr->cookieExpiration.count()));
455 #endif
456  }
457  cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
458  cookie.setPath(csrf->d_ptr->cookiePath);
459  cookie.setSecure(csrf->d_ptr->cookieSecure);
460  cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
461  c->res()->setCookie(cookie);
462  c->res()->headers().pushHeader("Vary"_qba, "Cookie"_qba);
463  }
464 
465  qCDebug(C_CSRFPROTECTION) << "Set token"
466  << c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
467  << "to" << (csrf->d_ptr->useSessions ? "session" : "cookie");
468 }
469 
475 void CSRFProtectionPrivate::reject(Context *c,
476  const QString &logReason,
477  const QString &displayReason)
478 {
479  c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, false);
480 
481  if (!csrf) {
482  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
483  return;
484  }
485 
486  if (C_CSRFPROTECTION().isWarningEnabled()) {
487  if (csrf->d_ptr->logFailedIp) {
488  qCWarning(C_CSRFPROTECTION).nospace().noquote()
489  << "Forbidden: (" << logReason << "): " << c->req()->path() << " ["
490  << c->req()->addressString() << "]";
491  } else {
492  qCWarning(C_CSRFPROTECTION).nospace().noquote()
493  << "Forbidden: (" << logReason << "): " << c->req()->path()
494  << " [IP logging disabled]";
495  }
496  }
497 
498  c->res()->setStatus(Response::Forbidden);
499  c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
500 
501  QString detachToCsrf = c->action()->attribute(u"CSRFDetachTo"_qs);
502  if (detachToCsrf.isEmpty()) {
503  detachToCsrf = csrf->d_ptr->defaultDetachTo;
504  }
505 
506  Action *detachToAction = nullptr;
507 
508  if (!detachToCsrf.isEmpty()) {
509  detachToAction = c->controller()->actionFor(detachToCsrf);
510  if (!detachToAction) {
511  detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
512  }
513  if (!detachToAction) {
514  qCWarning(C_CSRFPROTECTION)
515  << "Can not find action for" << detachToCsrf << "to detach to";
516  }
517  }
518 
519  if (detachToAction) {
520  c->detach(detachToAction);
521  } else {
522  c->res()->setStatus(Response::Forbidden);
523  if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
524  c->res()->setBody(csrf->d_ptr->genericErrorMessage);
525  c->res()->setContentType(csrf->d_ptr->genericContentType);
526  } else {
527  //% "403 Forbidden - CSRF protection check failed"
528  const QString title = c->qtTrId("cutelyst-csrf-generic-error-page-title");
529  c->res()->setBody(QStringLiteral("<!DOCTYPE html>\n"
530  "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
531  " <head>\n"
532  " <title>") +
533  title +
534  QStringLiteral("</title>\n"
535  " </head>\n"
536  " <body>\n"
537  " <h1>") +
538  title +
539  QStringLiteral("</h1>\n"
540  " <p>") +
541  displayReason +
542  QStringLiteral("</p>\n"
543  " </body>\n"
544  "</html>\n"));
545  c->res()->setContentType("text/html; charset=utf-8"_qba);
546  }
547  c->finalize();
548  }
549 }
550 
551 void CSRFProtectionPrivate::accept(Context *c)
552 {
553  c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, true);
554  c->setStash(CSRFProtectionPrivate::stashKeyProcessingDone, true);
555 }
556 
561 bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
562 {
563  const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
564  const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
565 
566  // to avoid timing attack
567  QByteArray::size_type diff = _t1.size() ^ _t2.size();
568  for (QByteArray::size_type i = 0; i < _t1.size() && i < _t2.size(); i++) {
569  diff |= _t1[i] ^ _t2[i];
570  }
571  return diff == 0;
572 }
573 
578 void CSRFProtectionPrivate::beforeDispatch(Context *c)
579 {
580  if (!csrf) {
581  CSRFProtectionPrivate::reject(c,
582  u"CSRFProtection plugin not registered"_qs,
583  //% "The CSRF protection plugin has not been registered."
584  c->qtTrId("cutelyst-csrf-reject-not-registered"));
585  return;
586  }
587 
588  const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
589  if (!csrfToken.isNull()) {
590  c->setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
591  } else {
592  CSRFProtection::getToken(c);
593  }
594 
595  if (c->stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
596  return;
597  }
598 
599  if (c->action()->attributes().contains(u"CSRFIgnore"_qs)) {
600  qCDebug(C_CSRFPROTECTION).noquote().nospace()
601  << "Action " << c->action()->className() << "::" << c->action()->reverse()
602  << " is ignored by the CSRF protection";
603  return;
604  }
605 
606  if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
607  if (!c->action()->attributes().contains(u"CSRFRequire"_qs)) {
608  qCDebug(C_CSRFPROTECTION)
609  << "Namespace" << c->action()->ns() << "is ignored by the CSRF protection";
610  return;
611  }
612  }
613 
614  // only check the tokens if the method is not secure, e.g. POST
615  // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
616  if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
617 
618  bool ok = true;
619 
620  // Suppose user visits http://example.com/
621  // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
622  // https://example.com/detonate-bomb/ and submits it via JavaScript.
623  //
624  // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
625  // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
626  // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
627  // For this reason, for https://example.com/ we need additional protection that treats
628  // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
629  // Referer header is missing for same-domain requests in only about 0.2% of cases or less,
630  // so we can use strict Referer checking.
631  if (c->req()->secure()) {
632  const auto referer = c->req()->headers().referer();
633 
634  if (Q_UNLIKELY(referer.isEmpty())) {
635  CSRFProtectionPrivate::reject(c,
636  u"Referer checking failed - no Referer"_qs,
637  //% "Referrer checking failed - no Referrer."
638  c->qtTrId("cutelyst-csrf-reject-no-referer"));
639  ok = false;
640  } else {
641  const QUrl refererUrl(QString::fromLatin1(referer));
642  if (Q_UNLIKELY(!refererUrl.isValid())) {
643  CSRFProtectionPrivate::reject(
644  c,
645  u"Referer checking failed - Referer is malformed"_qs,
646  //% "Referrer checking failed - Referrer is malformed."
647  c->qtTrId("cutelyst-csrf-reject-referer-malformed"));
648  ok = false;
649  } else {
650  if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String("https"))) {
651  CSRFProtectionPrivate::reject(
652  c,
653  u"Referer checking failed - Referer is insecure while "
654  "host is secure"_qs,
655  //% "Referrer checking failed - Referrer is insecure while host "
656  //% "is secure."
657  c->qtTrId("cutelyst-csrf-reject-refer-insecure"));
658  ok = false;
659  } else {
660  // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
661  // If not, obey the cookie rules (or those for the session cookie, if we
662  // use sessions
663  constexpr int httpPort = 80;
664  constexpr int httpsPort = 443;
665 
666  const QUrl uri = c->req()->uri();
667  QString goodReferer;
668  if (!csrf->d_ptr->useSessions) {
669  goodReferer = csrf->d_ptr->cookieDomain;
670  }
671  if (goodReferer.isEmpty()) {
672  goodReferer = uri.host();
673  }
674  const int serverPort = uri.port(c->req()->secure() ? httpsPort : httpPort);
675  if ((serverPort != httpPort) && (serverPort != httpsPort)) {
676  goodReferer += u':' + QString::number(serverPort);
677  }
678 
679  QStringList goodHosts = csrf->d_ptr->trustedOrigins;
680  goodHosts.append(goodReferer);
681 
682  QString refererHost = refererUrl.host();
683  const int refererPort = refererUrl.port(
684  refererUrl.scheme().compare(u"https") == 0 ? httpsPort : httpPort);
685  if ((refererPort != httpPort) && (refererPort != httpsPort)) {
686  refererHost += u':' + QString::number(refererPort);
687  }
688 
689  bool refererCheck = false;
690  for (const auto &host : std::as_const(goodHosts)) {
691  if ((host.startsWith(u'.') &&
692  (refererHost.endsWith(host) || (refererHost == host.mid(1)))) ||
693  host == refererHost) {
694  refererCheck = true;
695  break;
696  }
697  }
698 
699  if (Q_UNLIKELY(!refererCheck)) {
700  ok = false;
701  CSRFProtectionPrivate::reject(
702  c,
703  u"Referer checking failed - %1 does not match any "
704  "trusted origins"_qs.arg(QString::fromLatin1(referer)),
705  //% "Referrer checking failed - %1 does not match any "
706  //% "trusted origin."
707  c->qtTrId("cutelyst-csrf-reject-referer-no-trust")
708  .arg(QString::fromLatin1(referer)));
709  }
710  }
711  }
712  }
713  }
714 
715  if (Q_LIKELY(ok)) {
716  if (Q_UNLIKELY(csrfToken.isEmpty())) {
717  CSRFProtectionPrivate::reject(c,
718  u"CSRF cookie not set"_qs,
719  //% "CSRF cookie not set."
720  c->qtTrId("cutelyst-csrf-reject-no-cookie"));
721  ok = false;
722  } else {
723 
724  QByteArray requestCsrfToken;
725  // delete does not have body data
726  if (!c->req()->isDelete()) {
727  if (c->req()->contentType().compare("multipart/form-data") == 0) {
728  // everything is an upload, even our token
729  Upload *upload =
730  c->req()->upload(QString::fromLatin1(csrf->d_ptr->formInputName));
731  if (upload && upload->size() < 1024 /*FIXME*/) {
732  requestCsrfToken = upload->readAll();
733  }
734  } else
735  requestCsrfToken =
736  c->req()
737  ->bodyParam(QString::fromLatin1(csrf->d_ptr->formInputName))
738  .toLatin1();
739  }
740 
741  if (requestCsrfToken.isEmpty()) {
742  requestCsrfToken = c->req()->header(csrf->d_ptr->headerName);
743  if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
744  qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
745  << "from HTTP header" << csrf->d_ptr->headerName;
746  } else {
747  qCDebug(C_CSRFPROTECTION)
748  << "Can not get token from HTTP header or form field.";
749  }
750  } else {
751  qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
752  << "from form field" << csrf->d_ptr->formInputName;
753  }
754 
755  requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
756 
757  if (Q_UNLIKELY(
758  !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
759  CSRFProtectionPrivate::reject(c,
760  u"CSRF token missing or incorrect"_qs,
761  //% "CSRF token missing or incorrect."
762  c->qtTrId("cutelyst-csrf-reject-token-missin"));
763  ok = false;
764  }
765  }
766  }
767 
768  if (Q_LIKELY(ok)) {
769  CSRFProtectionPrivate::accept(c);
770  }
771  }
772 
773  // Set the CSRF cookie even if it's already set, so we renew
774  // the expiry timer.
775 
776  if (!c->stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
777  if (c->stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
778  return;
779  }
780  }
781 
782  if (!c->stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
783  return;
784  }
785 
786  CSRFProtectionPrivate::setToken(c);
787  c->setStash(CSRFProtectionPrivate::stashKeyCookieSet, true);
788 }
789 
790 #include "moc_csrfprotection.cpp"
ParamsMultiMap attributes() const noexcept
Definition: action.cpp:68
QByteArray toByteArray() const const
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:212
QByteArray header(QByteArrayView key) const noexcept
Definition: request.h:611
void postForked(Cutelyst::Application *app)
Upload * upload(QStringView name) const
Definition: request.h:626
void reserve(qsizetype size)
char at(qsizetype i) const const
Headers & headers() noexcept
QStringList split(const QString &sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool isDelete() const noexcept
Definition: request.cpp:354
Response * res() const noexcept
Definition: context.cpp:103
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:212
void detach(Action *action=nullptr)
Definition: context.cpp:339
Action * getActionByPath(QStringView path) const
Definition: dispatcher.cpp:232
Request req
Definition: context.h:67
int port(int defaultPort) const const
QString reverse() const noexcept
Definition: component.cpp:45
QByteArray referer() const noexcept
Definition: headers.cpp:310
void setContentType(const QByteArray &type)
Definition: response.h:238
This class represents a Cutelyst Action.
Definition: action.h:35
T value(qsizetype i) const const
int compare(QByteArrayView bv, Qt::CaseSensitivity cs) const const
Cutelyst Upload handles file upload requests.
Definition: upload.h:25
void resize(qsizetype size)
bool setup(Application *app) override
The Cutelyst Context.
Definition: context.h:42
Action action
Definition: context.h:48
QString number(int n, int base)
void append(QList::parameter_type value)
void setDomain(const QString &domain)
QString addressString() const
Definition: request.cpp:39
void stash(const QVariantHash &unite)
Definition: context.cpp:562
Headers headers() const noexcept
Definition: request.cpp:312
Controller controller
Definition: context.h:76
CaseInsensitive
QVariantMap config(const QString &entity) const
Definition: engine.cpp:263
bool isEmpty() const const
QByteArray readAll()
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:183
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
The Cutelyst namespace holds all public Cutelyst API.
QByteArray mid(qsizetype pos, qsizetype len) const const
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 pushHeader(const QByteArray &key, const QByteArray &value)
Definition: headers.cpp:458
bool contains(const Key &key) const const
QString fromLatin1(QByteArrayView str)
QByteArray left(qsizetype len) const const
QDateTime currentDateTime()
QByteArray toLatin1() const const
QString mid(qsizetype position, qsizetype n) const const
QString qtTrId(const char *id, int n=-1) const
Definition: context.h:657
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
CUTELYST_LIBRARY std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition: utils.cpp:291
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:168
Base class for Cutelyst Plugins.
Definition: plugin.h:24
The Cutelyst application.
Definition: application.h:72
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:277
Engine * engine() const noexcept
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Definition: request.h:571
Action * actionFor(QStringView name) const
Definition: controller.cpp:259
void setBody(QIODevice *body)
Definition: response.cpp:103
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition: action.cpp:74
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
int compare(const QString &other, Qt::CaseSensitivity cs) const const
void setStatus(quint16 status) noexcept
Definition: response.cpp:72
QString className() const noexcept
Definition: action.cpp:86
QUuid createUuid()
qint64 size() const override
Definition: upload.cpp:138
Dispatcher * dispatcher() const noexcept
Definition: context.cpp:139
void loadTranslations(const QString &filename, const QString &directory={}, const QString &prefix={}, const QString &suffix={})