6#include "csrfprotection_p.h"
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>
23#include <QLoggingCategory>
24#include <QNetworkCookie>
28#define DEFAULT_COOKIE_AGE Q_INT64_C(31449600)
29#define DEFAULT_COOKIE_NAME "csrftoken"
30#define DEFAULT_COOKIE_PATH "/"
31#define DEFAULT_COOKIE_SAMESITE "strict"
32#define DEFAULT_HEADER_NAME "X_CSRFTOKEN"
33#define DEFAULT_FORM_INPUT_NAME "csrfprotectiontoken"
34#define CSRF_SECRET_LENGTH 32
35#define CSRF_TOKEN_LENGTH 2 * CSRF_SECRET_LENGTH
36#define CSRF_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
37#define CSRF_SESSION_KEY "_csrftoken"
38#define CONTEXT_CSRF_COOKIE QStringLiteral("_c_csrfcookie")
39#define CONTEXT_CSRF_COOKIE_USED QStringLiteral("_c_csrfcookieused")
40#define CONTEXT_CSRF_COOKIE_NEEDS_RESET QStringLiteral("_c_csrfcookieneedsreset")
41#define CONTEXT_CSRF_PROCESSING_DONE QStringLiteral("_c_csrfprocessingdone")
42#define CONTEXT_CSRF_COOKIE_SET QStringLiteral("_c_csrfcookieset")
43#define CONTEXT_CSRF_CHECK_PASSED QStringLiteral("_c_csrfcheckpassed")
45Q_LOGGING_CATEGORY(C_CSRFPROTECTION,
"cutelyst.plugin.csrfprotection", QtWarningMsg)
54 QStringLiteral(
"GET"),
55 QStringLiteral(
"HEAD"),
56 QStringLiteral(
"OPTIONS"),
57 QStringLiteral(
"TRACE"),
62 , d_ptr(new CSRFProtectionPrivate)
77 const QVariantMap config =
78 app->
engine()->
config(QStringLiteral(
"Cutelyst_CSRFProtection_Plugin"));
80 d->cookieAge = config.value(QStringLiteral(
"cookie_age"), DEFAULT_COOKIE_AGE).value<qint64>();
81 if (d->cookieAge <= 0) {
82 d->cookieAge = DEFAULT_COOKIE_AGE;
84 d->cookieDomain = config.value(QStringLiteral(
"cookie_domain")).toString();
85 if (d->cookieName.isEmpty()) {
86 d->cookieName = QStringLiteral(DEFAULT_COOKIE_NAME);
88 d->cookiePath = QStringLiteral(DEFAULT_COOKIE_PATH);
89 d->cookieSecure = config.value(QStringLiteral(
"cookie_secure"),
false).toBool();
90 if (d->headerName.isEmpty()) {
91 d->headerName = QStringLiteral(DEFAULT_HEADER_NAME);
94 config.value(
QLatin1String(
"cookie_same_site"), QStringLiteral(
"strict")).toString();
95#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
97 d->cookieSameSite = QNetworkCookie::SameSite::Default;
99 d->cookieSameSite = QNetworkCookie::SameSite::None;
101 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
103 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
118 config.value(QStringLiteral(
"trusted_origins")).toString().split(u
',',
Qt::SkipEmptyParts);
119 if (d->formInputName.isEmpty()) {
120 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
122 d->logFailedIp = config.value(QStringLiteral(
"log_failed_ip"),
false).toBool();
123 if (d->errorMsgStashKey.isEmpty()) {
124 d->errorMsgStashKey = QStringLiteral(
"error_msg");
139 d->defaultDetachTo = actionNameOrPath;
146 d->formInputName = fieldName;
148 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
156 d->errorMsgStashKey = keyName;
158 d->errorMsgStashKey = QStringLiteral(
"error_msg");
165 d->ignoredNamespaces = namespaces;
171 d->useSessions = useSessions;
177 d->cookieHttpOnly = httpOnly;
183 d->cookieName = cookieName;
189 d->headerName = headerName;
195 d->genericErrorMessage = message;
201 d->genericContentType = type;
208 const QByteArray contextCookie = c->
stash(CONTEXT_CSRF_COOKIE).toByteArray();
211 secret = CSRFProtectionPrivate::getNewCsrfString();
212 token = CSRFProtectionPrivate::saltCipherSecret(secret);
213 c->
setStash(CONTEXT_CSRF_COOKIE, token);
215 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
216 token = CSRFProtectionPrivate::saltCipherSecret(secret);
219 c->
setStash(CONTEXT_CSRF_COOKIE_USED,
true);
229 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
233 form = QStringLiteral(
"<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
241 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
244 return c->
stash(CONTEXT_CSRF_CHECK_PASSED).toBool();
259QByteArray CSRFProtectionPrivate::getNewCsrfString()
263 while (csrfString.
size() < CSRF_SECRET_LENGTH) {
268 csrfString.
resize(CSRF_SECRET_LENGTH);
278QByteArray CSRFProtectionPrivate::saltCipherSecret(
const QByteArray &secret)
281 salted.
reserve(CSRF_TOKEN_LENGTH);
283 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
284 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
285 std::vector<std::pair<int, int>> pairs;
286 pairs.reserve(std::min(secret.
size(), salt.
size()));
287 for (
int i = 0; i < std::min(secret.
size(), salt.
size()); ++i) {
288 pairs.push_back(std::make_pair(chars.
indexOf(secret.
at(i)), chars.
indexOf(salt.
at(i))));
292 cipher.
reserve(CSRF_SECRET_LENGTH);
293 for (std::size_t i = 0; i < pairs.size(); ++i) {
294 const std::pair<int, int> p = pairs.at(i);
295 cipher.
append(chars[(p.first + p.second) % chars.
size()]);
298 salted = salt + cipher;
309QByteArray CSRFProtectionPrivate::unsaltCipherToken(
const QByteArray &token)
312 secret.
reserve(CSRF_SECRET_LENGTH);
314 const QByteArray salt = token.
left(CSRF_SECRET_LENGTH);
315 const QByteArray _token = token.
mid(CSRF_SECRET_LENGTH);
317 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
318 std::vector<std::pair<int, int>> pairs;
319 pairs.reserve(std::min(salt.
size(), _token.
size()));
320 for (
int i = 0; i < std::min(salt.
size(), _token.
size()); ++i) {
321 pairs.push_back(std::make_pair(chars.
indexOf(_token.
at(i)), chars.
indexOf(salt.
at(i))));
324 for (std::size_t i = 0; i < pairs.size(); ++i) {
325 const std::pair<int, int> p = pairs.at(i);
326 int idx = p.first - p.second;
328 idx = chars.
size() + idx;
341QByteArray CSRFProtectionPrivate::getNewCsrfToken()
343 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
351QByteArray CSRFProtectionPrivate::sanitizeToken(
const QByteArray &token)
353 QByteArray sanitized;
356 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe)) {
357 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
358 }
else if (token.
size() != CSRF_TOKEN_LENGTH) {
359 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
371QByteArray CSRFProtectionPrivate::getToken(
Context *c)
376 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
380 if (csrf->d_ptr->useSessions) {
383 QByteArray cookieToken = c->req()->
cookie(csrf->d_ptr->cookieName).
toLatin1();
389 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
390 if (token != cookieToken) {
391 c->
setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET,
true);
395 qCDebug(C_CSRFPROTECTION,
396 "Got token \"%s\" from %s.",
398 csrf->d_ptr->useSessions ?
"session" :
"cookie");
407void CSRFProtectionPrivate::setToken(
Context *c)
410 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
414 if (csrf->d_ptr->useSessions) {
416 c, QStringLiteral(CSRF_SESSION_KEY), c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
418#if (QT_VERSION >= QT_VERSION_CHECK(6, 1, 0))
419 QNetworkCookie cookie(csrf->d_ptr->cookieName.toLatin1(),
420 c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
422 Cookie cookie(csrf->d_ptr->cookieName.toLatin1(),
423 c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
425 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
426 cookie.setDomain(csrf->d_ptr->cookieDomain);
429 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
430 cookie.setPath(csrf->d_ptr->cookiePath);
431 cookie.setSecure(csrf->d_ptr->cookieSecure);
432 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
433#if (QT_VERSION >= QT_VERSION_CHECK(6, 1, 0))
436 c->
res()->setCuteCookie(cookie);
441 qCDebug(C_CSRFPROTECTION,
442 "Set token \"%s\" to %s.",
443 c->
stash(CONTEXT_CSRF_COOKIE).toByteArray().constData(),
444 csrf->d_ptr->useSessions ?
"session" :
"cookie");
452void CSRFProtectionPrivate::reject(
Context *c,
453 const QString &logReason,
454 const QString &displayReason)
456 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
false);
459 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
463 if (C_CSRFPROTECTION().isWarningEnabled()) {
464 if (csrf->d_ptr->logFailedIp) {
465 qCWarning(C_CSRFPROTECTION).nospace().noquote()
466 <<
"Forbidden: (" << logReason <<
"): " << c->req()->path() <<
" [client "
469 qCWarning(C_CSRFPROTECTION).nospace().noquote()
470 <<
"Forbidden: (" << logReason <<
"): " << c->req()->path();
475 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
477 QString detachToCsrf = c->action()->
attribute(QStringLiteral(
"CSRFDetachTo"));
479 detachToCsrf = csrf->d_ptr->defaultDetachTo;
482 Action *detachToAction =
nullptr;
485 detachToAction = c->controller()->
actionFor(detachToCsrf);
486 if (!detachToAction) {
489 if (!detachToAction) {
490 qCWarning(C_CSRFPROTECTION,
491 "Can not find action for \"%s\" to detach to.",
492 qPrintable(detachToCsrf));
496 if (detachToAction) {
497 c->
detach(detachToAction);
500 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
501 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
504 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
505 "403 Forbidden - CSRF protection check failed");
506 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
507 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
511 QStringLiteral(
"</title>\n"
516 QStringLiteral(
"</h1>\n"
519 QStringLiteral(
"</p>\n"
528void CSRFProtectionPrivate::accept(
Context *c)
530 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
531 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
538bool CSRFProtectionPrivate::compareSaltedTokens(
const QByteArray &t1,
const QByteArray &t2)
540 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
541 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
545 for (
int i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
546 diff |= _t1[i] ^ _t2[i];
555void CSRFProtectionPrivate::beforeDispatch(
Context *c)
558 CSRFProtectionPrivate::reject(
560 QStringLiteral(
"CSRFProtection plugin not registered"),
562 "The CSRF protection plugin has not been registered."));
566 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
567 if (!csrfToken.
isNull()) {
568 c->
setStash(CONTEXT_CSRF_COOKIE, csrfToken);
573 if (c->
stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
578 qCDebug(C_CSRFPROTECTION,
579 "Action \"%s::%s\" is ignored by the CSRF protection.",
581 qPrintable(c->action()->
reverse()));
585 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
587 qCDebug(C_CSRFPROTECTION,
588 "Namespace \"%s\" is ignored by the CSRF protection.",
589 qPrintable(c->action()->
ns()));
596 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
611 if (c->req()->secure()) {
614 if (Q_UNLIKELY(referer.
isEmpty())) {
615 CSRFProtectionPrivate::reject(
617 QStringLiteral(
"Referer checking failed - no Referer"),
619 "Referer checking failed - no Referer."));
622 const QUrl refererUrl(referer);
623 if (Q_UNLIKELY(!refererUrl.isValid())) {
624 CSRFProtectionPrivate::reject(
626 QStringLiteral(
"Referer checking failed - Referer is malformed"),
628 "Referer checking failed - Referer is malformed."));
631 if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String(
"https"))) {
632 CSRFProtectionPrivate::reject(
634 QStringLiteral(
"Referer checking failed - Referer is insecure while "
637 "Referer checking failed - Referer is insecure while host "
644 const QUrl uri = c->req()->uri();
646 if (!csrf->d_ptr->useSessions) {
647 goodReferer = csrf->d_ptr->cookieDomain;
650 goodReferer = uri.
host();
652 const int serverPort = uri.
port(c->req()->secure() ? 443 : 80);
653 if ((serverPort != 80) && (serverPort != 443)) {
657 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
658 goodHosts.
append(goodReferer);
660 QString refererHost = refererUrl.host();
661 const int refererPort =
662 refererUrl.port(refererUrl.scheme().compare(u
"https") == 0 ? 443 : 80);
663 if ((refererPort != 80) && (refererPort != 443)) {
667 bool refererCheck =
false;
668 for (
int i = 0; i < goodHosts.
size(); ++i) {
669 const QString host = goodHosts.
at(i);
671 (refererHost.
endsWith(host) || (refererHost == host.
mid(1)))) ||
672 host == refererHost) {
678 if (Q_UNLIKELY(!refererCheck)) {
680 CSRFProtectionPrivate::reject(
682 QStringLiteral(
"Referer checking failed - %1 does not match any "
686 "Referer checking failed - %1 does not match any "
696 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
697 CSRFProtectionPrivate::reject(
699 QStringLiteral(
"CSRF cookie not set"),
700 c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
704 QByteArray requestCsrfToken;
707 if (c->req()->contentType().
compare(u
"multipart/form-data") == 0) {
709 Upload *upload = c->req()->
upload(csrf->d_ptr->formInputName);
710 if (upload && upload->
size() < 1024 ) {
711 requestCsrfToken = upload->
readAll();
718 if (requestCsrfToken.
isEmpty()) {
719 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName).
toLatin1();
720 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
721 qCDebug(C_CSRFPROTECTION,
722 "Got token \"%s\" from HTTP header %s.",
724 qPrintable(csrf->d_ptr->headerName));
726 qCDebug(C_CSRFPROTECTION,
727 "Can not get token from HTTP header or form field.");
730 qCDebug(C_CSRFPROTECTION,
731 "Got token \"%s\" from form field %s.",
733 qPrintable(csrf->d_ptr->formInputName));
736 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
739 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
740 CSRFProtectionPrivate::reject(c,
741 QStringLiteral(
"CSRF token missing or incorrect"),
743 "CSRF token missing or incorrect."));
750 CSRFProtectionPrivate::accept(c);
757 if (!c->
stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
758 if (c->
stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
763 if (!c->
stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
767 CSRFProtectionPrivate::setToken(c);
768 c->
setStash(CONTEXT_CSRF_COOKIE_SET,
true);
771#include "moc_csrfprotection.cpp"
This class represents a Cutelyst Action.
QString ns() const noexcept
QString className() const
ParamsMultiMap attributes() const noexcept
QString attribute(const QString &name, const QString &defaultValue={}) const
The Cutelyst Application.
Engine * engine() const noexcept
void beforeDispatch(Cutelyst::Context *c)
T plugin()
Returns the registered plugin that casts to the template type T.
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)
void stash(const QVariantHash &unite)
void detach(Action *action=nullptr)
Response * res() const noexcept
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
void setStash(const QString &key, const QVariant &value)
Dispatcher * dispatcher() const noexcept
Action * actionFor(const QString &name) const
Action * getActionByPath(const QString &path) const
QVariantMap config(const QString &entity) const
user configuration for the application
Plugin(Application *parent)
QString addressString() const
QString header(const QString &key) const
bool isDelete() const noexcept
Headers headers() const noexcept
QString cookie(const QString &name) const
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Upload * upload(const QString &name) const
Headers & headers() noexcept
void setStatus(quint16 status) noexcept
void setBody(QIODevice *body)
void setCookie(const QNetworkCookie &cookie)
void setContentType(const QString &type)
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
static void setValue(Context *c, const QString &key, const QVariant &value)
Cutelyst Upload handles file upload request
virtual qint64 size() const override
The Cutelyst namespace holds all public Cutelyst API.
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
QDateTime currentDateTime()
void append(const T &value)
const T & at(int i) 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)
QObject * parent() const const
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
QString host(ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QByteArray toByteArray() const const