cutelyst  4.8.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 using namespace Qt::Literals::StringLiterals;
33 
34 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
35 static thread_local CSRFProtection *csrf = nullptr;
36 const QRegularExpression CSRFProtectionPrivate::sanitizeRe{u"[^a-zA-Z0-9\\-_]"_s};
37 // Assume that anything not defined as 'safe' by RFC7231 needs protection
38 const QByteArrayList CSRFProtectionPrivate::secureMethods = QByteArrayList({
39  "GET",
40  "HEAD",
41  "OPTIONS",
42  "TRACE",
43 });
44 const QByteArray CSRFProtectionPrivate::allowedChars{
45  "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"_ba};
46 const QString CSRFProtectionPrivate::sessionKey{u"_csrftoken"_s};
47 const QString CSRFProtectionPrivate::stashKeyCookie{u"_c_csrfcookie"_s};
48 const QString CSRFProtectionPrivate::stashKeyCookieUsed{u"_c_csrfcookieused"_s};
49 const QString CSRFProtectionPrivate::stashKeyCookieNeedsReset{u"_c_csrfcookieneedsreset"_s};
50 const QString CSRFProtectionPrivate::stashKeyCookieSet{u"_c_csrfcookieset"_s};
51 const QString CSRFProtectionPrivate::stashKeyProcessingDone{u"_c_csrfprocessingdone"_s};
52 const QString CSRFProtectionPrivate::stashKeyCheckPassed{u"_c_csrfcheckpassed"_s};
53 
54 CSRFProtection::CSRFProtection(Application *parent)
55  : Plugin(parent)
56  , d_ptr(new CSRFProtectionPrivate)
57 {
58 }
59 
60 CSRFProtection::CSRFProtection(Application *parent, const QVariantMap &defaultConfig)
61  : Plugin(parent)
62  , d_ptr(new CSRFProtectionPrivate)
63 {
64  Q_D(CSRFProtection);
65  d->defaultConfig = defaultConfig;
66 }
67 
68 CSRFProtection::~CSRFProtection() = default;
69 
71 {
72  Q_D(CSRFProtection);
73 
74  app->loadTranslations(u"plugin_csrfprotection"_s);
75 
76  const QVariantMap config = app->engine()->config(u"Cutelyst_CSRFProtection_Plugin"_s);
77 
78  bool cookieExpirationOk = false;
79  const QString cookieExpireStr =
80  config
81  .value(u"cookie_expiration"_s,
82  config.value(
83  u"cookie_age"_s,
84  d->defaultConfig.value(
85  u"cookie_expiration"_s,
86  static_cast<qint64>(std::chrono::duration_cast<std::chrono::seconds>(
87  CSRFProtectionPrivate::cookieDefaultExpiration)
88  .count()))))
89  .toString();
90  d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
91  Utils::durationFromString(cookieExpireStr, &cookieExpirationOk));
92  if (!cookieExpirationOk) {
93  qCWarning(C_CSRFPROTECTION).nospace() << "Invalid value set for cookie_expiration. "
94  "Using default value "
95 #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
96  << CSRFProtectionPrivate::cookieDefaultExpiration;
97 #else
98  << "1 year";
99 #endif
100  d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
101  }
102 
103  d->cookieDomain =
104  config.value(u"cookie_domain"_s, d->defaultConfig.value(u"cookie_domain"_s)).toString();
105  if (d->cookieName.isEmpty()) {
106  d->cookieName = "csrftoken";
107  }
108  d->cookiePath = u"/"_s;
109 
110  const QString _sameSite = config
111  .value(u"cookie_same_site"_s,
112  d->defaultConfig.value(u"cookie_same_site"_s, u"strict"_s))
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"_s, d->defaultConfig.value(u"cookie_secure"_s, 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"_s, d->defaultConfig.value(u"trusted_origins"_s))
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"_s, d->defaultConfig.value(u"log_failed_ip"_s, false))
154  .toBool();
155  if (d->errorMsgStashKey.isEmpty()) {
156  d->errorMsgStashKey = u"error_msg"_s;
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"_ba, "Cookie"_ba);
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"_s);
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"_ba);
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"_s,
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"_s)) {
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"_s)) {
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"_s,
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"_s,
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"_s,
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"_s.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"_s,
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"_s,
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"
ParamsMultiMap attributes() const noexcept
Definition: action.cpp:69
QByteArray toByteArray() const const
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:213
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
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool isDelete() const noexcept
Definition: request.cpp:355
Response * res() const noexcept
Definition: context.cpp:104
bool isNull() const const
QString host(ComponentFormattingOptions options) const const
bool isEmpty() const const
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:213
void detach(Action *action=nullptr)
Definition: context.cpp:340
Action * getActionByPath(QStringView path) const
Definition: dispatcher.cpp:233
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:311
void setContentType(const QByteArray &type)
Definition: response.h:238
This class represents a Cutelyst Action.
Definition: action.h:34
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
bool setup(Application *app) override
The Cutelyst Context.
Definition: context.h:42
Action action
Definition: context.h:48
QString number(double n, char format, int precision)
void setDomain(const QString &domain)
QString addressString() const
Definition: request.cpp:40
void stash(const QVariantHash &unite)
Definition: context.cpp:563
Headers headers() const noexcept
Definition: request.cpp:313
Controller controller
Definition: context.h:76
CaseInsensitive
QVariantMap config(const QString &entity) const
Definition: engine.cpp:263
bool isEmpty() const const
QByteArray readAll()
QDateTime currentDateTime()
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:184
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(QByteArrayView data)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString ns() const noexcept
Definition: action.cpp:119
SkipEmptyParts
void beforeDispatch(Cutelyst::Context *c)
void resize(qsizetype newSize, char c)
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition: headers.cpp:461
bool contains(const Key &key) const const
QString fromLatin1(QByteArrayView str)
QByteArray left(qsizetype len) const const
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
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
void append(QList< T > &&value)
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition: utils.cpp:291
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:169
Base class for Cutelyst Plugins.
Definition: plugin.h:24
The Cutelyst application.
Definition: application.h:72
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:278
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:260
void setBody(QIODevice *body)
Definition: response.cpp:104
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition: action.cpp:75
qsizetype size() const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QString arg(Args &&... args) const const
void setStatus(quint16 status) noexcept
Definition: response.cpp:73
QString className() const noexcept
Definition: action.cpp:87
QUuid createUuid()
qint64 size() const override
Definition: upload.cpp:138
Dispatcher * dispatcher() const noexcept
Definition: context.cpp:140
void loadTranslations(const QString &filename, const QString &directory={}, const QString &prefix={}, const QString &suffix={})