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 qCWarning(C_CSRFPROTECTION,
464 "Forbidden: (%s): /%s [%s]",
465 qPrintable(logReason),
466 qPrintable(c->req()->path()),
467 csrf->d_ptr->logFailedIp ? qPrintable(c->req()->
addressString())
468 :
"IP logging disabled");
471 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
473 QString detachToCsrf = c->action()->
attribute(QStringLiteral(
"CSRFDetachTo"));
475 detachToCsrf = csrf->d_ptr->defaultDetachTo;
478 Action *detachToAction =
nullptr;
481 detachToAction = c->controller()->
actionFor(detachToCsrf);
482 if (!detachToAction) {
485 if (!detachToAction) {
486 qCWarning(C_CSRFPROTECTION,
487 "Can not find action for \"%s\" to detach to.",
488 qPrintable(detachToCsrf));
492 if (detachToAction) {
493 c->
detach(detachToAction);
496 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
497 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
500 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
501 "403 Forbidden - CSRF protection check failed");
502 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
503 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
507 QStringLiteral(
"</title>\n"
512 QStringLiteral(
"</h1>\n"
515 QStringLiteral(
"</p>\n"
524void CSRFProtectionPrivate::accept(
Context *c)
526 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
527 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
534bool CSRFProtectionPrivate::compareSaltedTokens(
const QByteArray &t1,
const QByteArray &t2)
536 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
537 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
541 for (
int i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
542 diff |= _t1[i] ^ _t2[i];
551void CSRFProtectionPrivate::beforeDispatch(
Context *c)
554 CSRFProtectionPrivate::reject(
556 QStringLiteral(
"CSRFProtection plugin not registered"),
558 "The CSRF protection plugin has not been registered."));
562 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
563 if (!csrfToken.
isNull()) {
564 c->
setStash(CONTEXT_CSRF_COOKIE, csrfToken);
569 if (c->
stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
574 qCDebug(C_CSRFPROTECTION,
575 "Action \"%s::%s\" is ignored by the CSRF protection.",
577 qPrintable(c->action()->
reverse()));
581 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
583 qCDebug(C_CSRFPROTECTION,
584 "Namespace \"%s\" is ignored by the CSRF protection.",
585 qPrintable(c->action()->
ns()));
592 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
607 if (c->req()->secure()) {
610 if (Q_UNLIKELY(referer.
isEmpty())) {
611 CSRFProtectionPrivate::reject(
613 QStringLiteral(
"Referer checking failed - no Referer"),
615 "Referer checking failed - no Referer."));
618 const QUrl refererUrl(referer);
619 if (Q_UNLIKELY(!refererUrl.isValid())) {
620 CSRFProtectionPrivate::reject(
622 QStringLiteral(
"Referer checking failed - Referer is malformed"),
624 "Referer checking failed - Referer is malformed."));
627 if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String(
"https"))) {
628 CSRFProtectionPrivate::reject(
630 QStringLiteral(
"Referer checking failed - Referer is insecure while "
633 "Referer checking failed - Referer is insecure while host "
640 const QUrl uri = c->req()->uri();
642 if (!csrf->d_ptr->useSessions) {
643 goodReferer = csrf->d_ptr->cookieDomain;
646 goodReferer = uri.
host();
648 const int serverPort = uri.
port(c->req()->secure() ? 443 : 80);
649 if ((serverPort != 80) && (serverPort != 443)) {
653 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
654 goodHosts.
append(goodReferer);
656 QString refererHost = refererUrl.host();
657 const int refererPort =
658 refererUrl.port(refererUrl.scheme().compare(u
"https") == 0 ? 443 : 80);
659 if ((refererPort != 80) && (refererPort != 443)) {
663 bool refererCheck =
false;
664 for (
int i = 0; i < goodHosts.
size(); ++i) {
665 const QString host = goodHosts.
at(i);
667 (refererHost.
endsWith(host) || (refererHost == host.
mid(1)))) ||
668 host == refererHost) {
674 if (Q_UNLIKELY(!refererCheck)) {
676 CSRFProtectionPrivate::reject(
678 QStringLiteral(
"Referer checking failed - %1 does not match any "
682 "Referer checking failed - %1 does not match any "
692 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
693 CSRFProtectionPrivate::reject(
695 QStringLiteral(
"CSRF cookie not set"),
696 c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
700 QByteArray requestCsrfToken;
703 if (c->req()->contentType().
compare(u
"multipart/form-data") == 0) {
705 Upload *upload = c->req()->
upload(csrf->d_ptr->formInputName);
706 if (upload && upload->
size() < 1024 ) {
707 requestCsrfToken = upload->
readAll();
714 if (requestCsrfToken.
isEmpty()) {
715 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName).
toLatin1();
716 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
717 qCDebug(C_CSRFPROTECTION,
718 "Got token \"%s\" from HTTP header %s.",
720 qPrintable(csrf->d_ptr->headerName));
722 qCDebug(C_CSRFPROTECTION,
723 "Can not get token from HTTP header or form field.");
726 qCDebug(C_CSRFPROTECTION,
727 "Got token \"%s\" from form field %s.",
729 qPrintable(csrf->d_ptr->formInputName));
732 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
735 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
736 CSRFProtectionPrivate::reject(c,
737 QStringLiteral(
"CSRF token missing or incorrect"),
739 "CSRF token missing or incorrect."));
746 CSRFProtectionPrivate::accept(c);
753 if (!c->
stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
754 if (c->
stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
759 if (!c->
stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
763 CSRFProtectionPrivate::setToken(c);
764 c->
setStash(CONTEXT_CSRF_COOKIE_SET,
true);
767#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