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(QByteArrayView data)
char at(qsizetype i) const const
const char * constData() const const
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
bool isEmpty() const const
bool isNull() const const
QByteArray left(qsizetype len) &&
QByteArray mid(qsizetype pos, qsizetype len) &&
void reserve(qsizetype size)
void resize(qsizetype newSize, char c)
qsizetype size() const const
QDateTime currentDateTime()
void append(QList< T > &&value)
QList< T >::const_reference at(qsizetype i) const const
qsizetype size() const const
bool contains(const Key &key) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QString arg(Args &&... args) const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString mid(qsizetype position, qsizetype n) &&
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
QString host(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QByteArray toByteArray() const const