Cutelyst  3.5.0
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 <QNetworkCookie>
22 #include <QUuid>
23 #include <QUrl>
24 #include <vector>
25 #include <utility>
26 #include <algorithm>
27 
28 #define DEFAULT_COOKIE_AGE Q_INT64_C(31449600) // approx. 1 year
29 #define DEFAULT_COOKIE_NAME "csrftoken"
30 #define DEFAULT_COOKIE_PATH "/"
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 
86 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
87  d->trustedOrigins = config.value(QStringLiteral("trusted_origins")).toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
88 #else
89  d->trustedOrigins = config.value(QStringLiteral("trusted_origins")).toString().split(QLatin1Char(','), QString::SkipEmptyParts);
90 #endif
91  if (d->formInputName.isEmpty()) {
92  d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
93  }
94  d->logFailedIp = config.value(QStringLiteral("log_failed_ip"), false).toBool();
95  if (d->errorMsgStashKey.isEmpty()) {
96  d->errorMsgStashKey = QStringLiteral("error_msg");
97  }
98 
99  connect(app, &Application::postForked, this, [](Application *app){
100  csrf = app->plugin<CSRFProtection *>();
101  });
102 
103  connect(app, &Application::beforeDispatch, this, [d](Context *c) {
104  d->beforeDispatch(c);
105  });
106 
107  return true;
108 }
109 
110 void CSRFProtection::setDefaultDetachTo(const QString &actionNameOrPath)
111 {
112  Q_D(CSRFProtection);
113  d->defaultDetachTo = actionNameOrPath;
114 }
115 
117 {
118  Q_D(CSRFProtection);
119  if (!fieldName.isEmpty()) {
120  d->formInputName = fieldName;
121  } else {
122  d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
123  }
124 }
125 
127 {
128  Q_D(CSRFProtection);
129  if (!keyName.isEmpty()) {
130  d->errorMsgStashKey = keyName;
131  } else {
132  d->errorMsgStashKey = QStringLiteral("error_msg");
133  }
134 }
135 
137 {
138  Q_D(CSRFProtection);
139  d->ignoredNamespaces = namespaces;
140 }
141 
142 void CSRFProtection::setUseSessions(bool useSessions)
143 {
144  Q_D(CSRFProtection);
145  d->useSessions = useSessions;
146 }
147 
149 {
150  Q_D(CSRFProtection);
151  d->cookieHttpOnly = httpOnly;
152 }
153 
154 void CSRFProtection::setCookieName(const QString &cookieName)
155 {
156  Q_D(CSRFProtection);
157  d->cookieName = cookieName;
158 }
159 
160 void CSRFProtection::setHeaderName(const QString &headerName)
161 {
162  Q_D(CSRFProtection);
163  d->headerName = headerName;
164 }
165 
167 {
168  Q_D(CSRFProtection);
169  d->genericErrorMessage = message;
170 }
171 
173 {
174  Q_D(CSRFProtection);
175  d->genericContentType = type;
176 }
177 
179 {
180  QByteArray token;
181 
182  const QByteArray contextCookie = c->stash(CONTEXT_CSRF_COOKIE).toByteArray();
183  QByteArray secret;
184  if (contextCookie.isEmpty()) {
185  secret = CSRFProtectionPrivate::getNewCsrfString();
186  token = CSRFProtectionPrivate::saltCipherSecret(secret);
187  c->setStash(CONTEXT_CSRF_COOKIE, token);
188  } else {
189  secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
190  token = CSRFProtectionPrivate::saltCipherSecret(secret);
191  }
192 
193  c->setStash(CONTEXT_CSRF_COOKIE_USED, true);
194 
195  return token;
196 }
197 
199 {
200  QString form;
201 
202  if (!csrf) {
203  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
204  return form;
205  }
206 
207  form = QStringLiteral("<input type=\"hidden\" name=\"%1\" value=\"%2\" />").arg(csrf->d_ptr->formInputName, QString::fromLatin1(CSRFProtection::getToken(c)));
208 
209  return form;
210 }
211 
213 {
214  if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
215  return true;
216  } else {
217  return c->stash(CONTEXT_CSRF_CHECK_PASSED).toBool();
218  }
219 }
220 
221 //void CSRFProtection::rotateToken(Context *c)
222 //{
223 // c->setStash(CONTEXT_CSRF_COOKIE_USED, true);
224 // c->setStash(CONTEXT_CSRF_COOKIE, CSRFProtectionPrivate::getNewCsrfToken());
225 // c->setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET, true);
226 //}
227 
232 QByteArray CSRFProtectionPrivate::getNewCsrfString()
233 {
234  QByteArray csrfString;
235 
236  while (csrfString.size() < CSRF_SECRET_LENGTH) {
238  }
239 
240  csrfString.resize(CSRF_SECRET_LENGTH);
241 
242  return csrfString;
243 }
244 
250 QByteArray CSRFProtectionPrivate::saltCipherSecret(const QByteArray &secret)
251 {
252  QByteArray salted;
253  salted.reserve(CSRF_TOKEN_LENGTH);
254 
255  const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
256  const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
257  std::vector<std::pair<int,int>> pairs;
258  pairs.reserve(std::min(secret.size(), salt.size()));
259  for (int i = 0; i < std::min(secret.size(), salt.size()); ++i) {
260  pairs.push_back(std::make_pair(chars.indexOf(secret.at(i)), chars.indexOf(salt.at(i))));
261  }
262 
263  QByteArray cipher;
264  cipher.reserve(CSRF_SECRET_LENGTH);
265  for (std::size_t i = 0; i < pairs.size(); ++i) {
266  const std::pair<int,int> p = pairs.at(i);
267  cipher.append(chars[(p.first + p.second) % chars.size()]);
268  }
269 
270  salted = salt + cipher;
271 
272  return salted;
273 }
274 
281 QByteArray CSRFProtectionPrivate::unsaltCipherToken(const QByteArray &token)
282 {
283  QByteArray secret;
284  secret.reserve(CSRF_SECRET_LENGTH);
285 
286  const QByteArray salt = token.left(CSRF_SECRET_LENGTH);
287  const QByteArray _token = token.mid(CSRF_SECRET_LENGTH);
288 
289  const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
290  std::vector<std::pair<int,int>> pairs;
291  pairs.reserve(std::min(salt.size(), _token.size()));
292  for (int i = 0; i < std::min(salt.size(), _token.size()); ++i) {
293  pairs.push_back(std::make_pair(chars.indexOf(_token.at(i)), chars.indexOf(salt.at(i))));
294  }
295 
296 
297  for (std::size_t i = 0; i < pairs.size(); ++i) {
298  const std::pair<int,int> p = pairs.at(i);
299  int idx = p.first - p.second;
300  if (idx < 0) {
301  idx = chars.size() + idx;
302  }
303  secret.append(chars.at(idx));
304  }
305 
306  return secret;
307 }
308 
314 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
315 {
316  return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
317 }
318 
324 QByteArray CSRFProtectionPrivate::sanitizeToken(const QByteArray &token)
325 {
326  QByteArray sanitized;
327 
328  const QString tokenString = QString::fromLatin1(token);
329  if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe)) {
330  sanitized = CSRFProtectionPrivate::getNewCsrfToken();
331  } else if (token.size() != CSRF_TOKEN_LENGTH) {
332  sanitized = CSRFProtectionPrivate::getNewCsrfToken();
333  } else {
334  sanitized = token;
335  }
336 
337  return sanitized;
338 }
339 
344 QByteArray CSRFProtectionPrivate::getToken(Context *c)
345 {
346  QByteArray token;
347 
348  if (!csrf) {
349  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
350  return token;
351  }
352 
353  if (csrf->d_ptr->useSessions) {
354  token = Session::value(c, QStringLiteral(CSRF_SESSION_KEY)).toByteArray();
355  } else {
356  QByteArray cookieToken = c->req()->cookie(csrf->d_ptr->cookieName).toLatin1();
357 
358  if (cookieToken.isEmpty()) {
359  return token;
360  }
361 
362  token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
363  if (token != cookieToken) {
364  c->setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET, true);
365  }
366  }
367 
368  qCDebug(C_CSRFPROTECTION, "Got token \"%s\" from %s.", token.constData(), csrf->d_ptr->useSessions ? "session" : "cookie");
369 
370  return token;
371 }
372 
377 void CSRFProtectionPrivate::setToken(Context *c)
378 {
379  if (!csrf) {
380  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
381  return;
382  }
383 
384  if (csrf->d_ptr->useSessions) {
385  Session::setValue(c, QStringLiteral(CSRF_SESSION_KEY), c->stash(CONTEXT_CSRF_COOKIE).toByteArray());
386  } else {
387  QNetworkCookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->stash(CONTEXT_CSRF_COOKIE).toByteArray());
388  if (!csrf->d_ptr->cookieDomain.isEmpty()) {
389  cookie.setDomain(csrf->d_ptr->cookieDomain);
390  }
391  cookie.setExpirationDate(QDateTime::currentDateTime().addSecs(csrf->d_ptr->cookieAge));
392  cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
393  cookie.setPath(csrf->d_ptr->cookiePath);
394  cookie.setSecure(csrf->d_ptr->cookieSecure);
395  c->res()->setCookie(cookie);
396  c->res()->headers().pushHeader(QStringLiteral("Vary"), QStringLiteral("Cookie"));
397  }
398 
399  qCDebug(C_CSRFPROTECTION, "Set token \"%s\" to %s.", c->stash(CONTEXT_CSRF_COOKIE).toByteArray().constData(), csrf->d_ptr->useSessions ? "session" : "cookie");
400 }
401 
407 void CSRFProtectionPrivate::reject(Context *c, const QString &logReason, const QString &displayReason)
408 {
409  c->setStash(CONTEXT_CSRF_CHECK_PASSED, false);
410 
411  if (!csrf) {
412  qCCritical(C_CSRFPROTECTION) << "CSRFProtection plugin not registered";
413  return;
414  }
415 
416  qCWarning(C_CSRFPROTECTION, "Forbidden: (%s): /%s [%s]", qPrintable(logReason), qPrintable(c->req()->path()), csrf->d_ptr->logFailedIp ? qPrintable(c->req()->addressString()) : "IP logging disabled");
417 
418  c->res()->setStatus(Response::Forbidden);
419  c->setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
420 
421  QString detachToCsrf = c->action()->attribute(QStringLiteral("CSRFDetachTo"));
422  if (detachToCsrf.isEmpty()) {
423  detachToCsrf = csrf->d_ptr->defaultDetachTo;
424  }
425 
426  Action *detachToAction = nullptr;
427 
428  if (!detachToCsrf.isEmpty()) {
429  detachToAction = c->controller()->actionFor(detachToCsrf);
430  if (!detachToAction) {
431  detachToAction = c->dispatcher()->getActionByPath(detachToCsrf);
432  }
433  if (!detachToAction) {
434  qCWarning(C_CSRFPROTECTION, "Can not find action for \"%s\" to detach to.", qPrintable(detachToCsrf));
435  }
436  }
437 
438  if (detachToAction) {
439  c->detach(detachToAction);
440  } else {
441  c->res()->setStatus(403);
442  if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
443  c->res()->setBody(csrf->d_ptr->genericErrorMessage);
444  c->res()->setContentType(csrf->d_ptr->genericContentType);
445  } else {
446  const QString title = c->translate("Cutelyst::CSRFProtection", "403 Forbidden - CSRF protection check failed");
447  c->res()->setBody(QStringLiteral("<!DOCTYPE html>\n"
448  "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
449  " <head>\n"
450  " <title>") + title +
451  QStringLiteral("</title>\n"
452  " </head>\n"
453  " <body>\n"
454  " <h1>") + title +
455  QStringLiteral("</h1>\n"
456  " <p>") + displayReason +
457  QStringLiteral("</p>\n"
458  " </body>\n"
459  "</html>\n"));
460  c->res()->setContentType(QStringLiteral("text/html; charset=utf-8"));
461  }
462  c->detach();
463  }
464 }
465 
466 void CSRFProtectionPrivate::accept(Context *c)
467 {
468  c->setStash(CONTEXT_CSRF_CHECK_PASSED, true);
469  c->setStash(CONTEXT_CSRF_PROCESSING_DONE, true);
470 }
471 
476 bool CSRFProtectionPrivate::compareSaltedTokens(const QByteArray &t1, const QByteArray &t2)
477 {
478  const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
479  const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
480 
481  // to avoid timing attack
482  int diff = _t1.size() ^ _t2.size();
483  for (int i = 0; i < _t1.size() && i < _t2.size(); i++) {
484  diff |= _t1[i] ^ _t2[i];
485  }
486  return diff == 0;
487 }
488 
493 void CSRFProtectionPrivate::beforeDispatch(Context *c)
494 {
495  if (!csrf) {
496  CSRFProtectionPrivate::reject(c, QStringLiteral("CSRFProtection plugin not registered"), c->translate("Cutelyst::CSRFProtection", "The CSRF protection plugin has not been registered."));
497  return;
498  }
499 
500  const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
501  if (!csrfToken.isNull()) {
502  c->setStash(CONTEXT_CSRF_COOKIE, csrfToken);
503  } else {
505  }
506 
507  if (c->stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
508  return;
509  }
510 
511  if (c->action()->attributes().contains(QStringLiteral("CSRFIgnore"))) {
512  qCDebug(C_CSRFPROTECTION, "Action \"%s::%s\" is ignored by the CSRF protection.", qPrintable(c->action()->className()), qPrintable(c->action()->reverse()));
513  return;
514  }
515 
516  if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->ns())) {
517  if (!c->action()->attributes().contains(QStringLiteral("CSRFRequire"))) {
518  qCDebug(C_CSRFPROTECTION, "Namespace \"%s\" is ignored by the CSRF protection.", qPrintable(c->action()->ns()));
519  return;
520  }
521  }
522 
523  // only check the tokens if the method is not secure, e.g. POST
524  // the following methods are secure according to RFC 7231: GET, HEAD, OPTIONS and TRACE
525  if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
526 
527  bool ok = true;
528 
529  // Suppose user visits http://example.com/
530  // An active network attacker (man-in-the-middle, MITM) sends a POST form that targets
531  // https://example.com/detonate-bomb/ and submits it via JavaScript.
532  //
533  // The attacker will need to provide a CSRF cookie and token, but that's no problem for a
534  // MITM and the session-independent secret we're using. So the MITM can circumvent the CSRF
535  // protection. This is true for any HTTP connection, but anyone using HTTPS expects better!
536  // For this reason, for https://example.com/ we need additional protection that treats
537  // http://example.com/ as completely untrusted. Under HTTPS, Barth et al. found that the
538  // Referer header is missing for same-domain requests in only about 0.2% of cases or less, so
539  // we can use strict Referer checking.
540  if (c->req()->secure()) {
541  const QString referer = c->req()->headers().referer();
542 
543  if (Q_UNLIKELY(referer.isEmpty())) {
544  CSRFProtectionPrivate::reject(c, QStringLiteral("Referer checking failed - no Referer"), c->translate("Cutelyst::CSRFProtection", "Referer checking failed - no Referer."));
545  ok = false;
546  } else {
547  const QUrl refererUrl(referer);
548  if (Q_UNLIKELY(!refererUrl.isValid())) {
549  CSRFProtectionPrivate::reject(c, QStringLiteral("Referer checking failed - Referer is malformed"), c->translate("Cutelyst::CSRFProtection", "Referer checking failed - Referer is malformed."));
550  ok = false;
551  } else {
552  if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String("https"))) {
553  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."));
554  ok = false;
555  } else {
556  // If there isn't a CSRF_COOKIE_DOMAIN, require an exact match on host:port.
557  // If not, obey the cookie rules (or those for the session cookie, if we
558  // use sessions
559  const QUrl uri = c->req()->uri();
560  QString goodReferer;
561  if (!csrf->d_ptr->useSessions) {
562  goodReferer = csrf->d_ptr->cookieDomain;
563  }
564  if (goodReferer.isEmpty()) {
565  goodReferer = uri.host();
566  }
567  const int serverPort = uri.port(c->req()->secure() ? 443 : 80);
568  if ((serverPort != 80) && (serverPort != 443)) {
569  goodReferer += QLatin1Char(':') + QString::number(serverPort);
570  }
571 
572  QStringList goodHosts = csrf->d_ptr->trustedOrigins;
573  goodHosts.append(goodReferer);
574 
575  QString refererHost = refererUrl.host();
576  const int refererPort = refererUrl.port(refererUrl.scheme().compare(u"https") == 0 ? 443 : 80);
577  if ((refererPort != 80) && (refererPort != 443)) {
578  refererHost += QLatin1Char(':') + QString::number(refererPort);
579  }
580 
581  bool refererCheck = false;
582  for (int i = 0; i < goodHosts.size(); ++i) {
583  const QString host = goodHosts.at(i);
584  if ((host.startsWith(QLatin1Char('.')) && (refererHost.endsWith(host) || (refererHost == host.mid(1)))) || host == refererHost) {
585  refererCheck = true;
586  break;
587  }
588  }
589 
590  if (Q_UNLIKELY(!refererCheck)) {
591  ok = false;
592  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));
593  }
594  }
595  }
596  }
597  }
598 
599  if (Q_LIKELY(ok)) {
600  if (Q_UNLIKELY(csrfToken.isEmpty())) {
601  CSRFProtectionPrivate::reject(c, QStringLiteral("CSRF cookie not set"), c->translate("Cutelyst::CSRFProtection", "CSRF cookie not set."));
602  ok = false;
603  } else {
604 
605  QByteArray requestCsrfToken;
606  // delete does not have body data
607  if (!c->req()->isDelete()) {
608  if (c->req()->contentType().compare(u"multipart/form-data") == 0) {
609  // everything is an upload, even our token
610  Upload *upload = c->req()->upload(csrf->d_ptr->formInputName);
611  if (upload && upload->size() < 1024 /*FIXME*/) {
612  requestCsrfToken = upload->readAll();
613  }
614  } else
615  requestCsrfToken = c->req()->bodyParam(csrf->d_ptr->formInputName).toLatin1();
616  }
617 
618  if (requestCsrfToken.isEmpty()) {
619  requestCsrfToken = c->req()->header(csrf->d_ptr->headerName).toLatin1();
620  if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
621  qCDebug(C_CSRFPROTECTION, "Got token \"%s\" from HTTP header %s.", requestCsrfToken.constData(), qPrintable(csrf->d_ptr->headerName));
622  } else {
623  qCDebug(C_CSRFPROTECTION, "Can not get token from HTTP header or form field.");
624  }
625  } else {
626  qCDebug(C_CSRFPROTECTION, "Got token \"%s\" from form field %s.", requestCsrfToken.constData(), qPrintable(csrf->d_ptr->formInputName));
627  }
628 
629  requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
630 
631  if (Q_UNLIKELY(!CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
632  CSRFProtectionPrivate::reject(c, QStringLiteral("CSRF token missing or incorrect"), c->translate("Cutelyst::CSRFProtection", "CSRF token missing or incorrect."));
633  ok = false;
634  }
635  }
636  }
637 
638  if (Q_LIKELY(ok)) {
639  CSRFProtectionPrivate::accept(c);
640  }
641  }
642 
643  // Set the CSRF cookie even if it's already set, so we renew
644  // the expiry timer.
645 
646  if (!c->stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
647  if (c->stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
648  return;
649  }
650  }
651 
652  if (!c->stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
653  return;
654  }
655 
656  CSRFProtectionPrivate::setToken(c);
657  c->setStash(CONTEXT_CSRF_COOKIE_SET, true);
658 }
659 
660 #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:102
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:540
void detach(Action *action=nullptr)
Definition: context.cpp:333
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:471
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:212
Dispatcher * dispatcher() const noexcept
Definition: context.cpp:139
Action * actionFor(const QString &name) const
Definition: controller.cpp:37
Action * getActionByPath(const QString &path) const
Definition: dispatcher.cpp:220
QVariantMap config(const QString &entity) const
user configuration for the application
Definition: engine.cpp:307
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:215
void setContentType(const QString &type)
Definition: response.h:205
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:150
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:165
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
QByteArray & append(char ch)
char at(int i) const const
const char * constData() const const
int indexOf(char ch, int from) const const
bool isEmpty() const const
bool isNull() const const
QByteArray left(int len) const const
QByteArray mid(int pos, int len) const const
void reserve(int size)
void resize(int size)
int size() const const
QDateTime currentDateTime()
QByteArray readAll()
void append(const T &value)
const T & at(int i) const const
int size() const const
bool contains(const Key &key, const T &value) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
int compare(const QString &other, Qt::CaseSensitivity cs) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString fromLatin1(const char *str, int size)
bool isEmpty() const const
QString mid(int position, int n) const const
QString number(int n, int base)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
SkipEmptyParts
QString host(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QUuid createUuid()
QByteArray toByteArray() const const