cutelyst  3.7.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/Application>
9 #include <Cutelyst/Engine>
10 #include <Cutelyst/Context>
11 #include <Cutelyst/Request>
12 #include <Cutelyst/Response>
13 #include <Cutelyst/Plugins/Session/Session>
14 #include <Cutelyst/Headers>
15 #include <Cutelyst/Action>
16 #include <Cutelyst/Dispatcher>
17 #include <Cutelyst/Controller>
18 #include <Cutelyst/Upload>
19 
20 #include <QLoggingCategory>
21 #include <QUuid>
22 #include <QUrl>
23 #include <vector>
24 #include <utility>
25 #include <algorithm>
26 
27 #define DEFAULT_COOKIE_AGE Q_INT64_C(31449600) // approx. 1 year
28 #define DEFAULT_COOKIE_NAME "csrftoken"
29 #define DEFAULT_COOKIE_PATH "/"
30 #define DEFAULT_COOKIE_SAMESITE "strict"
31 #define DEFAULT_HEADER_NAME "X_CSRFTOKEN"
32 #define DEFAULT_FORM_INPUT_NAME "csrfprotectiontoken"
33 #define CSRF_SECRET_LENGTH 32
34 #define CSRF_TOKEN_LENGTH 2 * CSRF_SECRET_LENGTH
35 #define CSRF_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
36 #define CSRF_SESSION_KEY "_csrftoken"
37 #define CONTEXT_CSRF_COOKIE QStringLiteral("_c_csrfcookie")
38 #define CONTEXT_CSRF_COOKIE_USED QStringLiteral("_c_csrfcookieused")
39 #define CONTEXT_CSRF_COOKIE_NEEDS_RESET QStringLiteral("_c_csrfcookieneedsreset")
40 #define CONTEXT_CSRF_PROCESSING_DONE QStringLiteral("_c_csrfprocessingdone")
41 #define CONTEXT_CSRF_COOKIE_SET QStringLiteral("_c_csrfcookieset")
42 #define CONTEXT_CSRF_CHECK_PASSED QStringLiteral("_c_csrfcheckpassed")
43 
44 Q_LOGGING_CATEGORY(C_CSRFPROTECTION, "cutelyst.plugin.csrfprotection", QtWarningMsg)
45 
46 using namespace Cutelyst;
47 
48 static thread_local CSRFProtection *csrf = nullptr;
49 const QRegularExpression CSRFProtectionPrivate::sanitizeRe = QRegularExpression(QStringLiteral("[^a-zA-Z0-9\\-_]"));
50 // Assume that anything not defined as 'safe' by RFC7231 needs protection
51 const QStringList CSRFProtectionPrivate::secureMethods = QStringList({QStringLiteral("GET"), QStringLiteral("HEAD"), QStringLiteral("OPTIONS"), QStringLiteral("TRACE")});
52 
54  , d_ptr(new CSRFProtectionPrivate)
55 {
56 
57 }
58 
60 {
61  delete d_ptr;
62 }
63 
65 {
66  Q_D(CSRFProtection);
67 
68  app->loadTranslations(QStringLiteral("plugin_csrfprotection"));
69 
70  const QVariantMap config = app->engine()->config(QStringLiteral("Cutelyst_CSRFProtection_Plugin"));
71 
72  d->cookieAge = config.value(QStringLiteral("cookie_age"), DEFAULT_COOKIE_AGE).value<qint64>();
73  if (d->cookieAge <= 0) {
74  d->cookieAge = DEFAULT_COOKIE_AGE;
75  }
76  d->cookieDomain = config.value(QStringLiteral("cookie_domain")).toString();
77  if (d->cookieName.isEmpty()) {
78  d->cookieName = QStringLiteral(DEFAULT_COOKIE_NAME);
79  }
80  d->cookiePath = QStringLiteral(DEFAULT_COOKIE_PATH);
81  d->cookieSecure = config.value(QStringLiteral("cookie_secure"), false).toBool();
82  if (d->headerName.isEmpty()) {
83  d->headerName = QStringLiteral(DEFAULT_HEADER_NAME);
84  }
85  const QString _sameSite = config.value(QLatin1String("cookie_same_site"), QStringLiteral("strict")).toString();
86 #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
87  if (_sameSite.compare(u"default", Qt::CaseInsensitive) == 0) {
88  d->cookieSameSite = QNetworkCookie::SameSite::Default;
89  } else if (_sameSite.compare(u"none", Qt::CaseInsensitive) == 0) {
90  d->cookieSameSite = QNetworkCookie::SameSite::None;
91  } else if (_sameSite.compare(u"lax", Qt::CaseInsensitive) == 0) {
92  d->cookieSameSite = QNetworkCookie::SameSite::Lax;
93  } else {
94  d->cookieSameSite = QNetworkCookie::SameSite::Strict;
95  }
96 #else
97  if (_sameSite.compare(u"default", Qt::CaseInsensitive) == 0) {
98  d->cookieSameSite = Cookie::SameSite::Default;
99  } else if (_sameSite.compare(u"none", Qt::CaseInsensitive) == 0) {
100  d->cookieSameSite = Cookie::SameSite::None;
101  } else if (_sameSite.compare(u"lax", Qt::CaseInsensitive) == 0) {
102  d->cookieSameSite = Cookie::SameSite::Lax;
103  } else {
104  d->cookieSameSite = Cookie::SameSite::Strict;
105  }
106 #endif
107 
108  d->trustedOrigins = config.value(QStringLiteral("trusted_origins")).toString().split(u',', Qt::SkipEmptyParts);
109  if (d->formInputName.isEmpty()) {
110  d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
111  }
112  d->logFailedIp = config.value(QStringLiteral("log_failed_ip"), false).toBool();
113  if (d->errorMsgStashKey.isEmpty()) {
114  d->errorMsgStashKey = QStringLiteral("error_msg");
115  }
116 
117  connect(app, &Application::postForked, this, [](Application *app){
118  csrf = app->plugin<CSRFProtection *>();
119  });
120 
121  connect(app, &Application::beforeDispatch, this, [d](Context *c) {
122  d->beforeDispatch(c);
123  });
124 
125  return true;
126 }
127 
128 void CSRFProtection::setDefaultDetachTo(const QString &actionNameOrPath)
129 {
130  Q_D(CSRFProtection);
131  d->defaultDetachTo = actionNameOrPath;
132 }
133 
134 void CSRFProtection::setFormFieldName(const QString &fieldName)
135 {
136  Q_D(CSRFProtection);
137  if (!fieldName.isEmpty()) {
138  d->formInputName = fieldName;
139  } else {
140  d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
141  }
142 }
143 
144 void CSRFProtection::setErrorMsgStashKey(const QString &keyName)
145 {
146  Q_D(CSRFProtection);
147  if (!keyName.isEmpty()) {
148  d->errorMsgStashKey = keyName;
149  } else {
150  d->errorMsgStashKey = QStringLiteral("error_msg");
151  }
152 }
153 
154 void CSRFProtection::setIgnoredNamespaces(const QStringList &namespaces)
155 {
156  Q_D(CSRFProtection);
157  d->ignoredNamespaces = namespaces;
158 }
159 
160 void CSRFProtection::setUseSessions(bool useSessions)
161 {
162  Q_D(CSRFProtection);
163  d->useSessions = useSessions;
164 }
165 
167 {
168  Q_D(CSRFProtection);
169  d->cookieHttpOnly = httpOnly;
170 }
171 
172 void CSRFProtection::setCookieName(const QString &cookieName)
173 {
174  Q_D(CSRFProtection);
175  d->cookieName = cookieName;
176 }
177 
178 void CSRFProtection::setHeaderName(const QString &headerName)
179 {
180  Q_D(CSRFProtection);
181  d->headerName = headerName;
182 }
183 
184 void CSRFProtection::setGenericErrorMessage(const QString &message)
185 {
186  Q_D(CSRFProtection);
187  d->genericErrorMessage = message;
188 }
189 
191 {
192  Q_D(CSRFProtection);
193  d->genericContentType = type;
194 }
195 
197 {
198  QByteArray token;
199 
200  const QByteArray contextCookie = c->stash(CONTEXT_CSRF_COOKIE).toByteArray();
201  QByteArray secret;
202  if (contextCookie.isEmpty()) {
203  secret = CSRFProtectionPrivate::getNewCsrfString();
204  token = CSRFProtectionPrivate::saltCipherSecret(secret);
205  c->setStash(CONTEXT_CSRF_COOKIE, token);
206  } else {
207  secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
208  token = CSRFProtectionPrivate::saltCipherSecret(secret);
209  }
210 
211  c->setStash(CONTEXT_CSRF_COOKIE_USED, true);
212 
213  return token;
214 }
215 
217 {
218  QString form;
219 
220  if (!csrf) {
221  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
222  return form;
223  }
224 
225  form = QStringLiteral("<input type=\"hidden\" name=\"%1\" value=\"%2\" />").arg(csrf->d_ptr->formInputName, QString::fromLatin1(CSRFProtection::getToken(c)));
226 
227  return form;
228 }
229 
231 {
232  if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
233  return true;
234  } else {
235  return c->stash(CONTEXT_CSRF_CHECK_PASSED).toBool();
236  }
237 }
238 
239 //void CSRFProtection::rotateToken(Context *c)
240 //{
241 // c->setStash(CONTEXT_CSRF_COOKIE_USED, true);
242 // c->setStash(CONTEXT_CSRF_COOKIE, CSRFProtectionPrivate::getNewCsrfToken());
243 // c->setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET, true);
244 //}
245 
250 QByteArray CSRFProtectionPrivate::getNewCsrfString()
251 {
252  QByteArray csrfString;
253 
254  while (csrfString.size() < CSRF_SECRET_LENGTH) {
255  csrfString.append(QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
256  }
257 
258  csrfString.resize(CSRF_SECRET_LENGTH);
259 
260  return csrfString;
261 }
262 
268 QByteArray CSRFProtectionPrivate::saltCipherSecret(const QByteArray &secret)
269 {
270  QByteArray salted;
271  salted.reserve(CSRF_TOKEN_LENGTH);
272 
273  const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
274  const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
275  std::vector<std::pair<int,int>> pairs;
276  pairs.reserve(std::min(secret.size(), salt.size()));
277  for (int i = 0; i < std::min(secret.size(), salt.size()); ++i) {
278  pairs.push_back(std::make_pair(chars.indexOf(secret.at(i)), chars.indexOf(salt.at(i))));
279  }
280 
281  QByteArray cipher;
282  cipher.reserve(CSRF_SECRET_LENGTH);
283  for (std::size_t i = 0; i < pairs.size(); ++i) {
284  const std::pair<int,int> p = pairs.at(i);
285  cipher.append(chars[(p.first + p.second) % chars.size()]);
286  }
287 
288  salted = salt + cipher;
289 
290  return salted;
291 }
292 
299 QByteArray CSRFProtectionPrivate::unsaltCipherToken(const QByteArray &token)
300 {
301  QByteArray secret;
302  secret.reserve(CSRF_SECRET_LENGTH);
303 
304  const QByteArray salt = token.left(CSRF_SECRET_LENGTH);
305  const QByteArray _token = token.mid(CSRF_SECRET_LENGTH);
306 
307  const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
308  std::vector<std::pair<int,int>> pairs;
309  pairs.reserve(std::min(salt.size(), _token.size()));
310  for (int i = 0; i < std::min(salt.size(), _token.size()); ++i) {
311  pairs.push_back(std::make_pair(chars.indexOf(_token.at(i)), chars.indexOf(salt.at(i))));
312  }
313 
314 
315  for (std::size_t i = 0; i < pairs.size(); ++i) {
316  const std::pair<int,int> p = pairs.at(i);
317  int idx = p.first - p.second;
318  if (idx < 0) {
319  idx = chars.size() + idx;
320  }
321  secret.append(chars.at(idx));
322  }
323 
324  return secret;
325 }
326 
332 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
333 {
334  return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
335 }
336 
342 QByteArray CSRFProtectionPrivate::sanitizeToken(const QByteArray &token)
343 {
344  QByteArray sanitized;
345 
346  const QString tokenString = QString::fromLatin1(token);
347  if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe)) {
348  sanitized = CSRFProtectionPrivate::getNewCsrfToken();
349  } else if (token.size() != CSRF_TOKEN_LENGTH) {
350  sanitized = CSRFProtectionPrivate::getNewCsrfToken();
351  } else {
352  sanitized = token;
353  }
354 
355  return sanitized;
356 }
357 
362 QByteArray CSRFProtectionPrivate::getToken(Context *c)
363 {
364  QByteArray token;
365 
366  if (!csrf) {
367  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
368  return token;
369  }
370 
371  if (csrf->d_ptr->useSessions) {
372  token = Session::value(c, QStringLiteral(CSRF_SESSION_KEY)).toByteArray();
373  } else {
374  QByteArray cookieToken = c->req()->cookie(csrf->d_ptr->cookieName).toLatin1();
375 
376  if (cookieToken.isEmpty()) {
377  return token;
378  }
379 
380  token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
381  if (token != cookieToken) {
382  c->setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET, true);
383  }
384  }
385 
386  qCDebug(C_CSRFPROTECTION, "Got token \"%s\" from %s.", token.constData(), csrf->d_ptr->useSessions ? "session" : "cookie");
387 
388  return token;
389 }
390 
395 void CSRFProtectionPrivate::setToken(Context *c)
396 {
397  if (!csrf) {
398  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
399  return;
400  }
401 
402  if (csrf->d_ptr->useSessions) {
403  Session::setValue(c, QStringLiteral(CSRF_SESSION_KEY), c->stash(CONTEXT_CSRF_COOKIE).toByteArray());
404  } else {
405 #if (QT_VERSION >= QT_VERSION_CHECK(6, 1, 0))
406  QNetworkCookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->stash(CONTEXT_CSRF_COOKIE).toByteArray());
407 #else
408  Cookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->stash(CONTEXT_CSRF_COOKIE).toByteArray());
409 #endif
410  if (!csrf->d_ptr->cookieDomain.isEmpty()) {
411  cookie.setDomain(csrf->d_ptr->cookieDomain);
412  }
413  cookie.setExpirationDate(QDateTime::currentDateTime().addSecs(csrf->d_ptr->cookieAge));
414  cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
415  cookie.setPath(csrf->d_ptr->cookiePath);
416  cookie.setSecure(csrf->d_ptr->cookieSecure);
417  cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
418 #if (QT_VERSION >= QT_VERSION_CHECK(6, 1, 0))
419  c->res()->setCookie(cookie);
420 #else
421  c->res()->setCuteCookie(cookie);
422 #endif
423  c->res()->headers().pushHeader(QStringLiteral("Vary"), QStringLiteral("Cookie"));
424  }
425 
426  qCDebug(C_CSRFPROTECTION, "Set token \"%s\" to %s.", c->stash(CONTEXT_CSRF_COOKIE).toByteArray().constData(), csrf->d_ptr->useSessions ? "session" : "cookie");
427 }
428 
434 void CSRFProtectionPrivate::reject(Context *c, const QString &logReason, const QString &displayReason)
435 {
436  c->setStash(CONTEXT_CSRF_CHECK_PASSED, false);
437 
438  if (!csrf) {
439  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
440  return;
441  }
442 
443  qCWarning(C_CSRFPROTECTION, "Forbidden: (%s): /%s [%s]", qPrintable(logReason), qPrintable(c->req()->path()), csrf->d_ptr->logFailedIp ? qPrintable(c->req()->addressString()) : "IP logging disabled");
444 
445  c->res()->setStatus(Response::Forbidden);
446  c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
447 
448  QString detachToCsrf = c->action()->attribute(QStringLiteral("CSRFDetachTo"));
449  if (detachToCsrf.isEmpty()) {
450  detachToCsrf = csrf->d_ptr->defaultDetachTo;
451  }
452 
453  Action *detachToAction = nullptr;
454 
455  if (!detachToCsrf.isEmpty()) {
456  detachToAction = c->controller()->actionFor(detachToCsrf);
457  if (!detachToAction) {
458  detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
459  }
460  if (!detachToAction) {
461  qCWarning(C_CSRFPROTECTION, "Can not find action for \"%s\" to detach to.", qPrintable(detachToCsrf));
462  }
463  }
464 
465  if (detachToAction) {
466  c->detach(detachToAction);
467  } else {
468  c->res()->setStatus(403);
469  if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
470  c->res()->setBody(csrf->d_ptr->genericErrorMessage);
471  c->res()->setContentType(csrf->d_ptr->genericContentType);
472  } else {
473  const QString title = c->translate("Cutelyst::CSRFProtection", "403 Forbidden - CSRF protection check failed");
474  c->res()->setBody(QStringLiteral("<!DOCTYPE html>\n"
475  "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
476  " <head>\n"
477  " <title>") + title +
478  QStringLiteral("</title>\n"
479  " </head>\n"
480  " <body>\n"
481  " <h1>") + title +
482  QStringLiteral("</h1>\n"
483  " <p>") + displayReason +
484  QStringLiteral("</p>\n"
485  " </body>\n"
486  "</html>\n"));
487  c->res()->setContentType(QStringLiteral("text/html; charset=utf-8"));
488  }
489  c->detach();
490  }
491 }
492 
493 void CSRFProtectionPrivate::accept(Context *c)
494 {
495  c->setStash(CONTEXT_CSRF_CHECK_PASSED, true);
496  c->setStash(CONTEXT_CSRF_PROCESSING_DONE, true);
497 }
498 
503 bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
504 {
505  const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
506  const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
507 
508  // to avoid timing attack
509  int diff = _t1.size() ^ _t2.size();
510  for (int i = 0; i < _t1.size() && i < _t2.size(); i++) {
511  diff |= _t1[i] ^ _t2[i];
512  }
513  return diff == 0;
514 }
515 
520 void CSRFProtectionPrivate::beforeDispatch(Context *c)
521 {
522  if (!csrf) {
523  CSRFProtectionPrivate::reject(c, QStringLiteral("CSRFProtection plugin not registered"), c->translate("Cutelyst::CSRFProtection", "The CSRF protection plugin has not been registered."));
524  return;
525  }
526 
527  const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
528  if (!csrfToken.isNull()) {
529  c->setStash(CONTEXT_CSRF_COOKIE, csrfToken);
530  } else {
532  }
533 
534  if (c->stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
535  return;
536  }
537 
538  if (c->action()->attributes().contains(QStringLiteral("CSRFIgnore"))) {
539  qCDebug(C_CSRFPROTECTION, "Action \"%s::%s\" is ignored by the CSRF protection.", qPrintable(c->action()->className()), qPrintable(c->action()->reverse()));
540  return;
541  }
542 
543  if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
544  if (!c->action()->attributes().contains(QStringLiteral("CSRFRequire"))) {
545  qCDebug(C_CSRFPROTECTION, "Namespace \"%s\" is ignored by the CSRF protection.", qPrintable(c->action()->ns()));
546  return;
547  }
548  }
549 
550  // only check the tokens if the method is not secure, e.g. POST
551  // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
552  if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
553 
554  bool ok = true;
555 
556  // Suppose user visits http://example.com/
557  // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
558  // https://example.com/detonate-bomb/ and submits it via JavaScript.
559  //
560  // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
561  // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
562  // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
563  // For this reason, for https://example.com/ we need additional protection that treats
564  // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
565  // Referer header is missing for same-domain requests in only about 0.2% of cases or less, so
566  // we can use strict Referer checking.
567  if (c->req()->secure()) {
568  const QString referer = c->req()->headers().referer();
569 
570  if (Q_UNLIKELY(referer.isEmpty())) {
571  CSRFProtectionPrivate::reject(c, QStringLiteral("Referer checking failed - no Referer"), c->translate("Cutelyst::CSRFProtection", "Referer checking failed - no Referer."));
572  ok = false;
573  } else {
574  const QUrl refererUrl(referer);
575  if (Q_UNLIKELY(!refererUrl.isValid())) {
576  CSRFProtectionPrivate::reject(c, QStringLiteral("Referer checking failed - Referer is malformed"), c->translate("Cutelyst::CSRFProtection", "Referer checking failed - Referer is malformed."));
577  ok = false;
578  } else {
579  if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String("https"))) {
580  CSRFProtectionPrivate::reject(c, QStringLiteral("Referer checking failed - Referer is insecure while host is secure"), c->translate("Cutelyst::CSRFProtection", "Referer checking failed - Referer is insecure while host is secure."));
581  ok = false;
582  } else {
583  // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
584  // If not, obey the cookie rules (or those for the session cookie, if we
585  // use sessions
586  const QUrl uri = c->req()->uri();
587  QString goodReferer;
588  if (!csrf->d_ptr->useSessions) {
589  goodReferer = csrf->d_ptr->cookieDomain;
590  }
591  if (goodReferer.isEmpty()) {
592  goodReferer = uri.host();
593  }
594  const int serverPort = uri.port(c->req()->secure() ? 443 : 80);
595  if ((serverPort != 80) && (serverPort != 443)) {
596  goodReferer += u':' + QString::number(serverPort);
597  }
598 
599  QStringList goodHosts = csrf->d_ptr->trustedOrigins;
600  goodHosts.append(goodReferer);
601 
602  QString refererHost = refererUrl.host();
603  const int refererPort = refererUrl.port(refererUrl.scheme().compare(u"https") == 0 ? 443 : 80);
604  if ((refererPort != 80) && (refererPort != 443)) {
605  refererHost += u':' + QString::number(refererPort);
606  }
607 
608  bool refererCheck = false;
609  for (int i = 0; i < goodHosts.size(); ++i) {
610  const QString host = goodHosts.at(i);
611  if ((host.startsWith(u'.') && (refererHost.endsWith(host) || (refererHost == host.mid(1)))) || host == refererHost) {
612  refererCheck = true;
613  break;
614  }
615  }
616 
617  if (Q_UNLIKELY(!refererCheck)) {
618  ok = false;
619  CSRFProtectionPrivate::reject(c, QStringLiteral("Referer checking failed - %1 does not match any trusted origins").arg(referer), c->translate("Cutelyst::CSRFProtection", "Referer checking failed - %1 does not match any trusted origins.").arg(referer));
620  }
621  }
622  }
623  }
624  }
625 
626  if (Q_LIKELY(ok)) {
627  if (Q_UNLIKELY(csrfToken.isEmpty())) {
628  CSRFProtectionPrivate::reject(c, QStringLiteral("CSRF cookie not set"), c->translate("Cutelyst::CSRFProtection", "CSRF cookie not set."));
629  ok = false;
630  } else {
631 
632  QByteArray requestCsrfToken;
633  // delete does not have body data
634  if (!c->req()->isDelete()) {
635  if (c->req()->contentType().compare(u"multipart/form-data") == 0) {
636  // everything is an upload, even our token
637  Upload *upload = c->req()->upload(csrf->d_ptr->formInputName);
638  if (upload && upload->size() < 1024 /*FIXME*/) {
639  requestCsrfToken = upload->readAll();
640  }
641  } else
642  requestCsrfToken = c->req()->bodyParam(csrf->d_ptr->formInputName).toLatin1();
643  }
644 
645  if (requestCsrfToken.isEmpty()) {
646  requestCsrfToken = c->req()->header(csrf->d_ptr->headerName).toLatin1();
647  if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
648  qCDebug(C_CSRFPROTECTION, "Got token \"%s\" from HTTP header %s.", requestCsrfToken.constData(), qPrintable(csrf->d_ptr->headerName));
649  } else {
650  qCDebug(C_CSRFPROTECTION, "Can not get token from HTTP header or form field.");
651  }
652  } else {
653  qCDebug(C_CSRFPROTECTION, "Got token \"%s\" from form field %s.", requestCsrfToken.constData(), qPrintable(csrf->d_ptr->formInputName));
654  }
655 
656  requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
657 
658  if (Q_UNLIKELY(!CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
659  CSRFProtectionPrivate::reject(c, QStringLiteral("CSRF token missing or incorrect"), c->translate("Cutelyst::CSRFProtection", "CSRF token missing or incorrect."));
660  ok = false;
661  }
662  }
663  }
664 
665  if (Q_LIKELY(ok)) {
666  CSRFProtectionPrivate::accept(c);
667  }
668  }
669 
670  // Set the CSRF cookie even if it's already set, so we renew
671  // the expiry timer.
672 
673  if (!c->stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
674  if (c->stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
675  return;
676  }
677  }
678 
679  if (!c->stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
680  return;
681  }
682 
683  CSRFProtectionPrivate::setToken(c);
684  c->setStash(CONTEXT_CSRF_COOKIE_SET, true);
685 }
686 
687 #include "moc_csrfprotection.cpp"
This class represents a Cutelyst Action.
Definition: action.h:35
QString ns() const noexcept
Definition: action.cpp:116
QString className() const
Definition: action.cpp:84
ParamsMultiMap attributes() const noexcept
Definition: action.cpp:66
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition: action.cpp:72
The Cutelyst Application.
Definition: application.h:43
Engine * engine() const noexcept
void beforeDispatch(Cutelyst::Context *c)
T plugin()
Returns the registered plugin that casts to the template type T.
Definition: application.h:107
void loadTranslations(const QString &filename, const QString &directory=QString(), const QString &prefix=QString(), const QString &suffix=QString())
void postForked(Cutelyst::Application *app)
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
static bool checkPassed(Context *c)
void setUseSessions(bool useSessions)
void setIgnoredNamespaces(const QStringList &namespaces)
void setFormFieldName(const QString &fieldName)
void setDefaultDetachTo(const QString &actionNameOrPath)
void setErrorMsgStashKey(const QString &keyName)
void setCookieHttpOnly(bool httpOnly)
void setCookieName(const QString &cookieName)
void setGenericErrorContentTyp(const QString &type)
static QByteArray getToken(Context *c)
virtual ~CSRFProtection() override
void setGenericErrorMessage(const QString &message)
virtual bool setup(Application *app) override
static QString getTokenFormField(Context *c)
CSRFProtection(Application *parent)
void setHeaderName(const QString &headerName)
QString reverse() const
Definition: component.cpp:43
The Cutelyst Context.
Definition: context.h:39
void stash(const QVariantHash &unite)
Definition: context.cpp:546
void detach(Action *action=nullptr)
Definition: context.cpp:339
Response * res() const noexcept
Definition: context.cpp:103
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
Definition: context.cpp:477
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:218
Dispatcher * dispatcher() const noexcept
Definition: context.cpp:139
Action * actionFor(const QString &name) const
Definition: controller.cpp:37
The Cutelyst Cookie.
Definition: cookie.h:29
Action * getActionByPath(const QString &path) const
Definition: dispatcher.cpp:220
QVariantMap config(const QString &entity) const
user configuration for the application
Definition: engine.cpp:292
QString referer() const
Definition: headers.cpp:310
void pushHeader(const QString &field, const QString &value)
Definition: headers.cpp:410
QString addressString() const
Definition: request.cpp:39
QString header(const QString &key) const
Definition: request.h:554
bool isDelete() const noexcept
Definition: request.cpp:350
Headers headers() const noexcept
Definition: request.cpp:308
QString cookie(const QString &name) const
Definition: request.cpp:272
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Definition: request.h:530
Upload * upload(const QString &name) const
Definition: request.h:563
void setStatus(quint16 status) noexcept
Definition: response.cpp:72
Headers & headers() noexcept
void setBody(QIODevice *body)
Definition: response.cpp:101
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:231
void setContentType(const QString &type)
Definition: response.h:220
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:172
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:187
Cutelyst Upload handles file upload request
Definition: upload.h:23
virtual qint64 size() const override
Definition: upload.cpp:140
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:8