cutelyst  4.4.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 
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 #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
460  cookie.setExpirationDate(
461  QDateTime::currentDateTime().addDuration(csrf->d_ptr->cookieExpiration));
462 #else
463  cookie.setExpirationDate(
464  QDateTime::currentDateTime().addSecs(csrf->d_ptr->cookieExpiration.count()));
465 #endif
466  }
467  cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
468  cookie.setPath(csrf->d_ptr->cookiePath);
469  cookie.setSecure(csrf->d_ptr->cookieSecure);
470  cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
471  c->res()->setCookie(cookie);
472  c->res()->headers().pushHeader("Vary"_qba, "Cookie"_qba);
473  }
474 
475  qCDebug(C_CSRFPROTECTION) << "Set token"
476  << c->stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
477  << "to" << (csrf->d_ptr->useSessions ? "session" : "cookie");
478 }
479 
485 void CSRFProtectionPrivate::reject(Context *c,
486  const QString &logReason,
487  const QString &displayReason)
488 {
489  c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, false);
490 
491  if (!csrf) {
492  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
493  return;
494  }
495 
496  if (C_CSRFPROTECTION().isWarningEnabled()) {
497  if (csrf->d_ptr->logFailedIp) {
498  qCWarning(C_CSRFPROTECTION).nospace().noquote()
499  << "Forbidden: (" << logReason << "): " << c->req()->path() << " ["
500  << c->req()->addressString() << "]";
501  } else {
502  qCWarning(C_CSRFPROTECTION).nospace().noquote()
503  << "Forbidden: (" << logReason << "): " << c->req()->path()
504  << " [IP logging disabled]";
505  }
506  }
507 
508  c->res()->setStatus(Response::Forbidden);
509  c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
510 
511  QString detachToCsrf = c->action()->attribute(u"CSRFDetachTo"_qs);
512  if (detachToCsrf.isEmpty()) {
513  detachToCsrf = csrf->d_ptr->defaultDetachTo;
514  }
515 
516  Action *detachToAction = nullptr;
517 
518  if (!detachToCsrf.isEmpty()) {
519  detachToAction = c->controller()->actionFor(detachToCsrf);
520  if (!detachToAction) {
521  detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
522  }
523  if (!detachToAction) {
524  qCWarning(C_CSRFPROTECTION)
525  << "Can not find action for" << detachToCsrf << "to detach to";
526  }
527  }
528 
529  if (detachToAction) {
530  c->detach(detachToAction);
531  } else {
532  c->res()->setStatus(Response::Forbidden);
533  if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
534  c->res()->setBody(csrf->d_ptr->genericErrorMessage);
535  c->res()->setContentType(csrf->d_ptr->genericContentType);
536  } else {
537  //% "403 Forbidden - CSRF protection check failed"
538  const QString title = c->qtTrId("cutelyst-csrf-generic-error-page-title");
539  c->res()->setBody(QStringLiteral("<!DOCTYPE html>\n"
540  "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
541  " <head>\n"
542  " <title>") +
543  title +
544  QStringLiteral("</title>\n"
545  " </head>\n"
546  " <body>\n"
547  " <h1>") +
548  title +
549  QStringLiteral("</h1>\n"
550  " <p>") +
551  displayReason +
552  QStringLiteral("</p>\n"
553  " </body>\n"
554  "</html>\n"));
555  c->res()->setContentType("text/html; charset=utf-8"_qba);
556  }
557  c->finalize();
558  }
559 }
560 
561 void CSRFProtectionPrivate::accept(Context *c)
562 {
563  c->setStash(CSRFProtectionPrivate::stashKeyCheckPassed, true);
564  c->setStash(CSRFProtectionPrivate::stashKeyProcessingDone, true);
565 }
566 
571 bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
572 {
573  const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
574  const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
575 
576  // to avoid timing attack
577  QByteArray::size_type diff = _t1.size() ^ _t2.size();
578  for (QByteArray::size_type i = 0; i < _t1.size() && i < _t2.size(); i++) {
579  diff |= _t1[i] ^ _t2[i];
580  }
581  return diff == 0;
582 }
583 
588 void CSRFProtectionPrivate::beforeDispatch(Context *c)
589 {
590  if (!csrf) {
591  CSRFProtectionPrivate::reject(c,
592  u"CSRFProtection plugin not registered"_qs,
593  //% "The CSRF protection plugin has not been registered."
594  c->qtTrId("cutelyst-csrf-reject-not-registered"));
595  return;
596  }
597 
598  const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
599  if (!csrfToken.isNull()) {
600  c->setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
601  } else {
602  CSRFProtection::getToken(c);
603  }
604 
605  if (c->stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
606  return;
607  }
608 
609  if (c->action()->attributes().contains(u"CSRFIgnore"_qs)) {
610  qCDebug(C_CSRFPROTECTION).noquote().nospace()
611  << "Action " << c->action()->className() << "::" << c->action()->reverse()
612  << " is ignored by the CSRF protection";
613  return;
614  }
615 
616  if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
617  if (!c->action()->attributes().contains(u"CSRFRequire"_qs)) {
618  qCDebug(C_CSRFPROTECTION)
619  << "Namespace" << c->action()->ns() << "is ignored by the CSRF protection";
620  return;
621  }
622  }
623 
624  // only check the tokens if the method is not secure, e.g. POST
625  // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
626  if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
627 
628  bool ok = true;
629 
630  // Suppose user visits http://example.com/
631  // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
632  // https://example.com/detonate-bomb/ and submits it via JavaScript.
633  //
634  // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
635  // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
636  // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
637  // For this reason, for https://example.com/ we need additional protection that treats
638  // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
639  // Referer header is missing for same-domain requests in only about 0.2% of cases or less,
640  // so we can use strict Referer checking.
641  if (c->req()->secure()) {
642  const auto referer = c->req()->headers().referer();
643 
644  if (Q_UNLIKELY(referer.isEmpty())) {
645  CSRFProtectionPrivate::reject(c,
646  u"Referer checking failed - no Referer"_qs,
647  //% "Referrer checking failed - no Referrer."
648  c->qtTrId("cutelyst-csrf-reject-no-referer"));
649  ok = false;
650  } else {
651  const QUrl refererUrl(QString::fromLatin1(referer));
652  if (Q_UNLIKELY(!refererUrl.isValid())) {
653  CSRFProtectionPrivate::reject(
654  c,
655  u"Referer checking failed - Referer is malformed"_qs,
656  //% "Referrer checking failed - Referrer is malformed."
657  c->qtTrId("cutelyst-csrf-reject-referer-malformed"));
658  ok = false;
659  } else {
660  if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String("https"))) {
661  CSRFProtectionPrivate::reject(
662  c,
663  u"Referer checking failed - Referer is insecure while "
664  "host is secure"_qs,
665  //% "Referrer checking failed - Referrer is insecure while host "
666  //% "is secure."
667  c->qtTrId("cutelyst-csrf-reject-refer-insecure"));
668  ok = false;
669  } else {
670  // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
671  // If not, obey the cookie rules (or those for the session cookie, if we
672  // use sessions
673  constexpr int httpPort = 80;
674  constexpr int httpsPort = 443;
675 
676  const QUrl uri = c->req()->uri();
677  QString goodReferer;
678  if (!csrf->d_ptr->useSessions) {
679  goodReferer = csrf->d_ptr->cookieDomain;
680  }
681  if (goodReferer.isEmpty()) {
682  goodReferer = uri.host();
683  }
684  const int serverPort = uri.port(c->req()->secure() ? httpsPort : httpPort);
685  if ((serverPort != httpPort) && (serverPort != httpsPort)) {
686  goodReferer += u':' + QString::number(serverPort);
687  }
688 
689  QStringList goodHosts = csrf->d_ptr->trustedOrigins;
690  goodHosts.append(goodReferer);
691 
692  QString refererHost = refererUrl.host();
693  const int refererPort = refererUrl.port(
694  refererUrl.scheme().compare(u"https") == 0 ? httpsPort : httpPort);
695  if ((refererPort != httpPort) && (refererPort != httpsPort)) {
696  refererHost += u':' + QString::number(refererPort);
697  }
698 
699  bool refererCheck = false;
700  for (const auto &host : std::as_const(goodHosts)) {
701  if ((host.startsWith(u'.') &&
702  (refererHost.endsWith(host) || (refererHost == host.mid(1)))) ||
703  host == refererHost) {
704  refererCheck = true;
705  break;
706  }
707  }
708 
709  if (Q_UNLIKELY(!refererCheck)) {
710  ok = false;
711  CSRFProtectionPrivate::reject(
712  c,
713  u"Referer checking failed - %1 does not match any "
714  "trusted origins"_qs.arg(QString::fromLatin1(referer)),
715  //% "Referrer checking failed - %1 does not match any "
716  //% "trusted origin."
717  c->qtTrId("cutelyst-csrf-reject-referer-no-trust")
718  .arg(QString::fromLatin1(referer)));
719  }
720  }
721  }
722  }
723  }
724 
725  if (Q_LIKELY(ok)) {
726  if (Q_UNLIKELY(csrfToken.isEmpty())) {
727  CSRFProtectionPrivate::reject(c,
728  u"CSRF cookie not set"_qs,
729  //% "CSRF cookie not set."
730  c->qtTrId("cutelyst-csrf-reject-no-cookie"));
731  ok = false;
732  } else {
733 
734  QByteArray requestCsrfToken;
735  // delete does not have body data
736  if (!c->req()->isDelete()) {
737  if (c->req()->contentType().compare("multipart/form-data") == 0) {
738  // everything is an upload, even our token
739  Upload *upload =
740  c->req()->upload(QString::fromLatin1(csrf->d_ptr->formInputName));
741  if (upload && upload->size() < 1024 /*FIXME*/) {
742  requestCsrfToken = upload->readAll();
743  }
744  } else
745  requestCsrfToken =
746  c->req()
747  ->bodyParam(QString::fromLatin1(csrf->d_ptr->formInputName))
748  .toLatin1();
749  }
750 
751  if (requestCsrfToken.isEmpty()) {
752  requestCsrfToken = c->req()->header(csrf->d_ptr->headerName);
753  if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
754  qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
755  << "from HTTP header" << csrf->d_ptr->headerName;
756  } else {
757  qCDebug(C_CSRFPROTECTION)
758  << "Can not get token from HTTP header or form field.";
759  }
760  } else {
761  qCDebug(C_CSRFPROTECTION) << "Got token" << requestCsrfToken
762  << "from form field" << csrf->d_ptr->formInputName;
763  }
764 
765  requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
766 
767  if (Q_UNLIKELY(
768  !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
769  CSRFProtectionPrivate::reject(c,
770  u"CSRF token missing or incorrect"_qs,
771  //% "CSRF token missing or incorrect."
772  c->qtTrId("cutelyst-csrf-reject-token-missin"));
773  ok = false;
774  }
775  }
776  }
777 
778  if (Q_LIKELY(ok)) {
779  CSRFProtectionPrivate::accept(c);
780  }
781  }
782 
783  // Set the CSRF cookie even if it's already set, so we renew
784  // the expiry timer.
785 
786  if (!c->stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
787  if (c->stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
788  return;
789  }
790  }
791 
792  if (!c->stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
793  return;
794  }
795 
796  CSRFProtectionPrivate::setToken(c);
797  c->setStash(CSRFProtectionPrivate::stashKeyCookieSet, true);
798 }
799 
800 #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