cutelyst  4.5.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 <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 
69 bool CSRFProtection::setup(Application *app)
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 QByteArray CSRFProtection::formFieldName() noexcept
181 {
182  if (!csrf) {
183  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
184  return {};
185  }
186 
187  return csrf->d_ptr->formInputName;
188 }
189 
190 void CSRFProtection::setErrorMsgStashKey(const QString &keyName)
191 {
192  Q_D(CSRFProtection);
193  d->errorMsgStashKey = keyName;
194 }
195 
196 void CSRFProtection::setIgnoredNamespaces(const QStringList &namespaces)
197 {
198  Q_D(CSRFProtection);
199  d->ignoredNamespaces = namespaces;
200 }
201 
202 void CSRFProtection::setUseSessions(bool useSessions)
203 {
204  Q_D(CSRFProtection);
205  d->useSessions = useSessions;
206 }
207 
208 void CSRFProtection::setCookieHttpOnly(bool httpOnly)
209 {
210  Q_D(CSRFProtection);
211  d->cookieHttpOnly = httpOnly;
212 }
213 
214 void CSRFProtection::setCookieName(const QByteArray &cookieName)
215 {
216  Q_D(CSRFProtection);
217  d->cookieName = cookieName;
218 }
219 
220 void CSRFProtection::setHeaderName(const QByteArray &headerName)
221 {
222  Q_D(CSRFProtection);
223  d->headerName = headerName;
224 }
225 
226 void CSRFProtection::setGenericErrorMessage(const QString &message)
227 {
228  Q_D(CSRFProtection);
229  d->genericErrorMessage = message;
230 }
231 
232 void CSRFProtection::setGenericErrorContentType(const QByteArray &type)
233 {
234  Q_D(CSRFProtection);
235  d->genericContentType = type;
236 }
237 
238 QByteArray CSRFProtection::getToken(Context *c)
239 {
240  QByteArray token;
241 
242  const QByteArray contextCookie = c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
243  QByteArray secret;
244  if (contextCookie.isEmpty()) {
245  secret = CSRFProtectionPrivate::getNewCsrfString();
246  token = CSRFProtectionPrivate::saltCipherSecret(secret);
247  c->setStash(CSRFProtectionPrivate::stashKeyCookie, token);
248  } else {
249  secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
250  token = CSRFProtectionPrivate::saltCipherSecret(secret);
251  }
252 
253  c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
254 
255  return token;
256 }
257 
258 QString CSRFProtection::getTokenFormField(Context *c)
259 {
260  QString form;
261 
262  if (!csrf) {
263  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
264  return form;
265  }
266 
267  form = QStringLiteral("<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
268  .arg(QString::fromLatin1(CSRFProtection::formFieldName()),
269  QString::fromLatin1(CSRFProtection::getToken(c)));
270 
271  return form;
272 }
273 
274 bool CSRFProtection::checkPassed(Context *c)
275 {
276  if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
277  return true;
278  } else {
279  return c->stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
280  }
281 }
282 
283 // void CSRFProtection::rotateToken(Context *c)
284 //{
285 // c->setStash(CSRFProtectionPrivate::stashKeyCookieUsed, true);
286 // c->setStash(QString CSRFProtectionPrivate::stashKeyCookie,
287 // CSRFProtectionPrivate::getNewCsrfToken());
288 // c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
289 // }
290 
295 QByteArray CSRFProtectionPrivate::getNewCsrfString()
296 {
297  QByteArray csrfString;
298 
299  while (csrfString.size() < CSRFProtectionPrivate::secretLength) {
300  csrfString.append(QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding |
302  }
303 
304  csrfString.resize(CSRFProtectionPrivate::secretLength);
305 
306  return csrfString;
307 }
308 
314 QByteArray CSRFProtectionPrivate::saltCipherSecret(const QByteArray &secret)
315 {
316  QByteArray salted;
317  salted.reserve(CSRFProtectionPrivate::tokenLength);
318 
319  const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
320  std::vector<std::pair<int, int>> pairs;
321  pairs.reserve(std::min(secret.size(), salt.size()));
322  for (int i = 0; i < std::min(secret.size(), salt.size()); ++i) {
323  pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.at(i)),
324  CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
325  }
326 
327  QByteArray cipher;
328  cipher.reserve(CSRFProtectionPrivate::secretLength);
329  for (const auto &p : std::as_const(pairs)) {
330  cipher.append(
331  CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
332  CSRFProtectionPrivate::allowedChars.size()]);
333  }
334 
335  salted = salt + cipher;
336 
337  return salted;
338 }
339 
346 QByteArray CSRFProtectionPrivate::unsaltCipherToken(const QByteArray &token)
347 {
348  QByteArray secret;
349  secret.reserve(CSRFProtectionPrivate::secretLength);
350 
351  const QByteArray salt = token.left(CSRFProtectionPrivate::secretLength);
352  const QByteArray _token = token.mid(CSRFProtectionPrivate::secretLength);
353 
354  std::vector<std::pair<int, int>> pairs;
355  pairs.reserve(std::min(salt.size(), _token.size()));
356  for (int i = 0; i < std::min(salt.size(), _token.size()); ++i) {
357  pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.at(i)),
358  CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
359  }
360 
361  for (const auto &p : std::as_const(pairs)) {
362  QByteArray::size_type idx = p.first - p.second;
363  if (idx < 0) {
364  idx = CSRFProtectionPrivate::allowedChars.size() + idx;
365  }
366  secret.append(CSRFProtectionPrivate::allowedChars.at(idx));
367  }
368 
369  return secret;
370 }
371 
377 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
378 {
379  return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
380 }
381 
387 QByteArray CSRFProtectionPrivate::sanitizeToken(const QByteArray &token)
388 {
389  QByteArray sanitized;
390 
391  const QString tokenString = QString::fromLatin1(token);
392  if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe) ||
393  token.size() != CSRFProtectionPrivate::tokenLength) {
394  sanitized = CSRFProtectionPrivate::getNewCsrfToken();
395  } else {
396  sanitized = token;
397  }
398 
399  return sanitized;
400 }
401 
406 QByteArray CSRFProtectionPrivate::getToken(Context *c)
407 {
408  QByteArray token;
409 
410  if (!csrf) {
411  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
412  return token;
413  }
414 
415  if (csrf->d_ptr->useSessions) {
416  token = Session::value(c, CSRFProtectionPrivate::sessionKey).toByteArray();
417  } else {
418  QByteArray cookieToken = c->req()->cookie(csrf->d_ptr->cookieName);
419  if (cookieToken.isEmpty()) {
420  return token;
421  }
422 
423  token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
424  if (token != cookieToken) {
425  c->setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset, true);
426  }
427  }
428 
429  qCDebug(C_CSRFPROTECTION) << "Got token" << token << "from"
430  << (csrf->d_ptr->useSessions ? "sessions" : "cookie");
431 
432  return token;
433 }
434 
439 void CSRFProtectionPrivate::setToken(Context *c)
440 {
441  if (!csrf) {
442  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
443  return;
444  }
445 
446  if (csrf->d_ptr->useSessions) {
448  CSRFProtectionPrivate::sessionKey,
449  c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
450  } else {
451  QNetworkCookie cookie(csrf->d_ptr->cookieName,
452  c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
453  if (!csrf->d_ptr->cookieDomain.isEmpty()) {
454  cookie.setDomain(csrf->d_ptr->cookieDomain);
455  }
456  if (csrf->d_ptr->cookieExpiration.count() == 0) {
457  cookie.setExpirationDate(QDateTime());
458  } else {
459  cookie.setExpirationDate(
460  QDateTime::currentDateTime().addDuration(csrf->d_ptr->cookieExpiration));
461  }
462  cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
463  cookie.setPath(csrf->d_ptr->cookiePath);
464  cookie.setSecure(csrf->d_ptr->cookieSecure);
465  cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
466  c->res()->setCookie(cookie);
467  c->res()->headers().pushHeader("Vary"_qba, "Cookie"_qba);
468  }
469 
470  qCDebug(C_CSRFPROTECTION) << "Set token"
471  << c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
472  << "to" << (csrf->d_ptr->useSessions ? "session" : "cookie");
473 }
474 
480 void CSRFProtectionPrivate::reject(Context *c,
481  const QString &logReason,
482  const QString &displayReason)
483 {
484  c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, false);
485 
486  if (!csrf) {
487  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
488  return;
489  }
490 
491  if (C_CSRFPROTECTION().isWarningEnabled()) {
492  if (csrf->d_ptr->logFailedIp) {
493  qCWarning(C_CSRFPROTECTION).nospace().noquote()
494  << "Forbidden: (" << logReason << "): " << c->req()->path() << " ["
495  << c->req()->addressString() << "]";
496  } else {
497  qCWarning(C_CSRFPROTECTION).nospace().noquote()
498  << "Forbidden: (" << logReason << "): " << c->req()->path()
499  << " [IP logging disabled]";
500  }
501  }
502 
503  c->res()->setStatus(Response::Forbidden);
504  c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
505 
506  QString detachToCsrf = c->action()->attribute(u"CSRFDetachTo"_qs);
507  if (detachToCsrf.isEmpty()) {
508  detachToCsrf = csrf->d_ptr->defaultDetachTo;
509  }
510 
511  Action *detachToAction = nullptr;
512 
513  if (!detachToCsrf.isEmpty()) {
514  detachToAction = c->controller()->actionFor(detachToCsrf);
515  if (!detachToAction) {
516  detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
517  }
518  if (!detachToAction) {
519  qCWarning(C_CSRFPROTECTION)
520  << "Can not find action for" << detachToCsrf << "to detach to";
521  }
522  }
523 
524  if (detachToAction) {
525  c->detach(detachToAction);
526  } else {
527  c->res()->setStatus(Response::Forbidden);
528  if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
529  c->res()->setBody(csrf->d_ptr->genericErrorMessage);
530  c->res()->setContentType(csrf->d_ptr->genericContentType);
531  } else {
532  //% "403 Forbidden - CSRF protection check failed"
533  const QString title = c->qtTrId("cutelyst-csrf-generic-error-page-title");
534  c->res()->setBody(QStringLiteral("<!DOCTYPE html>\n"
535  "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
536  " <head>\n"
537  " <title>") +
538  title +
539  QStringLiteral("</title>\n"
540  " </head>\n"
541  " <body>\n"
542  " <h1>") +
543  title +
544  QStringLiteral("</h1>\n"
545  " <p>") +
546  displayReason +
547  QStringLiteral("</p>\n"
548  " </body>\n"
549  "</html>\n"));
550  c->res()->setContentType("text/html; charset=utf-8"_qba);
551  }
552  c->finalize();
553  }
554 }
555 
556 void CSRFProtectionPrivate::accept(Context *c)
557 {
558  c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, true);
559  c->setStash(CSRFProtectionPrivate::stashKeyProcessingDone, true);
560 }
561 
566 bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
567 {
568  const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
569  const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
570 
571  // to avoid timing attack
572  QByteArray::size_type diff = _t1.size() ^ _t2.size();
573  for (QByteArray::size_type i = 0; i < _t1.size() && i < _t2.size(); i++) {
574  diff |= _t1[i] ^ _t2[i];
575  }
576  return diff == 0;
577 }
578 
583 void CSRFProtectionPrivate::beforeDispatch(Context *c)
584 {
585  if (!csrf) {
586  CSRFProtectionPrivate::reject(c,
587  u"CSRFProtection plugin not registered"_qs,
588  //% "The CSRF protection plugin has not been registered."
589  c->qtTrId("cutelyst-csrf-reject-not-registered"));
590  return;
591  }
592 
593  const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
594  if (!csrfToken.isNull()) {
595  c->setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
596  } else {
597  CSRFProtection::getToken(c);
598  }
599 
600  if (c->stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
601  return;
602  }
603 
604  if (c->action()->attributes().contains(u"CSRFIgnore"_qs)) {
605  qCDebug(C_CSRFPROTECTION).noquote().nospace()
606  << "Action " << c->action()->className() << "::" << c->action()->reverse()
607  << " is ignored by the CSRF protection";
608  return;
609  }
610 
611  if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
612  if (!c->action()->attributes().contains(u"CSRFRequire"_qs)) {
613  qCDebug(C_CSRFPROTECTION)
614  << "Namespace" << c->action()->ns() << "is ignored by the CSRF protection";
615  return;
616  }
617  }
618 
619  // only check the tokens if the method is not secure, e.g. POST
620  // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
621  if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
622 
623  bool ok = true;
624 
625  // Suppose user visits http://example.com/
626  // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
627  // https://example.com/detonate-bomb/ and submits it via JavaScript.
628  //
629  // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
630  // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
631  // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
632  // For this reason, for https://example.com/ we need additional protection that treats
633  // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
634  // Referer header is missing for same-domain requests in only about 0.2% of cases or less,
635  // so we can use strict Referer checking.
636  if (c->req()->secure()) {
637  const auto referer = c->req()->headers().referer();
638 
639  if (Q_UNLIKELY(referer.isEmpty())) {
640  CSRFProtectionPrivate::reject(c,
641  u"Referer checking failed - no Referer"_qs,
642  //% "Referrer checking failed - no Referrer."
643  c->qtTrId("cutelyst-csrf-reject-no-referer"));
644  ok = false;
645  } else {
646  const QUrl refererUrl(QString::fromLatin1(referer));
647  if (Q_UNLIKELY(!refererUrl.isValid())) {
648  CSRFProtectionPrivate::reject(
649  c,
650  u"Referer checking failed - Referer is malformed"_qs,
651  //% "Referrer checking failed - Referrer is malformed."
652  c->qtTrId("cutelyst-csrf-reject-referer-malformed"));
653  ok = false;
654  } else {
655  if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String("https"))) {
656  CSRFProtectionPrivate::reject(
657  c,
658  u"Referer checking failed - Referer is insecure while "
659  "host is secure"_qs,
660  //% "Referrer checking failed - Referrer is insecure while host "
661  //% "is secure."
662  c->qtTrId("cutelyst-csrf-reject-refer-insecure"));
663  ok = false;
664  } else {
665  // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
666  // If not, obey the cookie rules (or those for the session cookie, if we
667  // use sessions
668  constexpr int httpPort = 80;
669  constexpr int httpsPort = 443;
670 
671  const QUrl uri = c->req()->uri();
672  QString goodReferer;
673  if (!csrf->d_ptr->useSessions) {
674  goodReferer = csrf->d_ptr->cookieDomain;
675  }
676  if (goodReferer.isEmpty()) {
677  goodReferer = uri.host();
678  }
679  const int serverPort = uri.port(c->req()->secure() ? httpsPort : httpPort);
680  if ((serverPort != httpPort) && (serverPort != httpsPort)) {
681  goodReferer += u':' + QString::number(serverPort);
682  }
683 
684  QStringList goodHosts = csrf->d_ptr->trustedOrigins;
685  goodHosts.append(goodReferer);
686 
687  QString refererHost = refererUrl.host();
688  const int refererPort = refererUrl.port(
689  refererUrl.scheme().compare(u"https") == 0 ? httpsPort : httpPort);
690  if ((refererPort != httpPort) && (refererPort != httpsPort)) {
691  refererHost += u':' + QString::number(refererPort);
692  }
693 
694  bool refererCheck = false;
695  for (const auto &host : std::as_const(goodHosts)) {
696  if ((host.startsWith(u'.') &&
697  (refererHost.endsWith(host) || (refererHost == host.mid(1)))) ||
698  host == refererHost) {
699  refererCheck = true;
700  break;
701  }
702  }
703 
704  if (Q_UNLIKELY(!refererCheck)) {
705  ok = false;
706  CSRFProtectionPrivate::reject(
707  c,
708  u"Referer checking failed - %1 does not match any "
709  "trusted origins"_qs.arg(QString::fromLatin1(referer)),
710  //% "Referrer checking failed - %1 does not match any "
711  //% "trusted origin."
712  c->qtTrId("cutelyst-csrf-reject-referer-no-trust")
713  .arg(QString::fromLatin1(referer)));
714  }
715  }
716  }
717  }
718  }
719 
720  if (Q_LIKELY(ok)) {
721  if (Q_UNLIKELY(csrfToken.isEmpty())) {
722  CSRFProtectionPrivate::reject(c,
723  u"CSRF cookie not set"_qs,
724  //% "CSRF cookie not set."
725  c->qtTrId("cutelyst-csrf-reject-no-cookie"));
726  ok = false;
727  } else {
728 
729  QByteArray requestCsrfToken;
730  // delete does not have body data
731  if (!c->req()->isDelete()) {
732  if (c->req()->contentType().compare("multipart/form-data") == 0) {
733  // everything is an upload, even our token
734  Upload *upload =
735  c->req()->upload(QString::fromLatin1(csrf->d_ptr->formInputName));
736  if (upload && upload->size() < 1024 /*FIXME*/) {
737  requestCsrfToken = upload->readAll();
738  }
739  } else
740  requestCsrfToken =
741  c->req()
742  ->bodyParam(QString::fromLatin1(csrf->d_ptr->formInputName))
743  .toLatin1();
744  }
745 
746  if (requestCsrfToken.isEmpty()) {
747  requestCsrfToken = c->req()->header(csrf->d_ptr->headerName);
748  if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
749  qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
750  << "from HTTP header" << csrf->d_ptr->headerName;
751  } else {
752  qCDebug(C_CSRFPROTECTION)
753  << "Can not get token from HTTP header or form field.";
754  }
755  } else {
756  qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
757  << "from form field" << csrf->d_ptr->formInputName;
758  }
759 
760  requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
761 
762  if (Q_UNLIKELY(
763  !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
764  CSRFProtectionPrivate::reject(c,
765  u"CSRF token missing or incorrect"_qs,
766  //% "CSRF token missing or incorrect."
767  c->qtTrId("cutelyst-csrf-reject-token-missin"));
768  ok = false;
769  }
770  }
771  }
772 
773  if (Q_LIKELY(ok)) {
774  CSRFProtectionPrivate::accept(c);
775  }
776  }
777 
778  // Set the CSRF cookie even if it's already set, so we renew
779  // the expiry timer.
780 
781  if (!c->stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
782  if (c->stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
783  return;
784  }
785  }
786 
787  if (!c->stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
788  return;
789  }
790 
791  CSRFProtectionPrivate::setToken(c);
792  c->setStash(CSRFProtectionPrivate::stashKeyCookieSet, true);
793 }
794 
795 #include "moc_csrfprotection.cpp"
This class represents a Cutelyst Action.
Definition: action.h:35
QString ns() const noexcept
Definition: action.cpp:118
QString className() const noexcept
Definition: action.cpp:86
ParamsMultiMap attributes() const noexcept
Definition: action.cpp:68
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition: action.cpp:74
The Cutelyst application.
Definition: application.h:66
Engine * engine() const noexcept
void beforeDispatch(Cutelyst::Context *c)
void loadTranslations(const QString &filename, const QString &directory={}, const QString &prefix={}, const QString &suffix={})
void postForked(Cutelyst::Application *app)
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
QString reverse() const noexcept
Definition: component.cpp:45
The Cutelyst Context.
Definition: context.h:42
void stash(const QVariantHash &unite)
Definition: context.cpp:562
void detach(Action *action=nullptr)
Definition: context.cpp:339
Response * res() const noexcept
Definition: context.cpp:103
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:212
Request * req
Definition: context.h:66
Controller * controller
Definition: context.h:75
QString qtTrId(const char *id, int n=-1) const
Definition: context.h:656
Action * action
Definition: context.h:47
Dispatcher * dispatcher() const noexcept
Definition: context.cpp:139
Action * actionFor(QStringView name) const
Definition: controller.cpp:259
Action * getActionByPath(QStringView path) const
Definition: dispatcher.cpp:232
QVariantMap config(const QString &entity) const
Definition: engine.cpp:263
QByteArray referer() const noexcept
Definition: headers.cpp:310
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition: headers.cpp:460
Base class for Cutelyst Plugins.
Definition: plugin.h:25
QString addressString() const
Definition: request.cpp:39
bool isDelete() const noexcept
Definition: request.cpp:354
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:277
Upload * upload(QStringView name) const
Definition: request.h:626
Headers headers() const noexcept
Definition: request.cpp:312
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Definition: request.h:571
QByteArray header(QByteArrayView key) const noexcept
Definition: request.h:611
void setContentType(const QByteArray &type)
Definition: response.h:238
void setStatus(quint16 status) noexcept
Definition: response.cpp:72
Headers & headers() noexcept
void setBody(QIODevice *body)
Definition: response.cpp:103
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:212
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:168
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:183
Cutelyst Upload handles file upload requests.
Definition: upload.h:26
qint64 size() const override
Definition: upload.cpp:138
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition: utils.cpp:291
The Cutelyst namespace holds all public Cutelyst API.
QByteArray & append(QByteArrayView data)
char at(qsizetype i) const const
int compare(QByteArrayView bv, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool isNull() const const
QByteArray left(qsizetype len) const const
QByteArray mid(qsizetype pos, qsizetype len) const const
void reserve(qsizetype size)
void resize(qsizetype newSize, char c)
qsizetype size() const const
QDateTime currentDateTime()
QByteArray readAll()
void append(QList::parameter_type value)
T value(qsizetype i) const const
bool contains(const Key &key) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString arg(Args &&... args) const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QStringList split(QChar sep, Qt::SplitBehavior behavior, 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