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) // approx. 1 year 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") 45 Q_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();
259 QByteArray CSRFProtectionPrivate::getNewCsrfString()
263 while (csrfString.
size() < CSRF_SECRET_LENGTH) {
268 csrfString.
resize(CSRF_SECRET_LENGTH);
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;
312 secret.
reserve(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;
341 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
343 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
356 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe)) {
357 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
358 }
else if (token.
size() != CSRF_TOKEN_LENGTH) {
359 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
376 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
380 if (csrf->d_ptr->useSessions) {
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");
407 void 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)) 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");
452 void CSRFProtectionPrivate::reject(
Context *c,
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);
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" 524 void CSRFProtectionPrivate::accept(
Context *c)
526 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
527 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
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];
551 void 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) {
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."));
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" ParamsMultiMap attributes() const noexcept
QByteArray toByteArray() const const
void setCookie(const QNetworkCookie &cookie)
void setHeaderName(const QString &headerName)
void postForked(Cutelyst::Application *app)
void setContentType(const QString &type)
char at(int i) const const
Headers & headers() noexcept
bool isDelete() const noexcept
Response * res() const noexcept
const T & at(int i) const const
bool isNull() const const
QString host(QUrl::ComponentFormattingOptions options) const const
bool isEmpty() const const
void setStash(const QString &key, const QVariant &value)
void detach(Action *action=nullptr)
void loadTranslations(const QString &filename, const QString &directory=QString(), const QString &prefix=QString(), const QString &suffix=QString())
Action * actionFor(const QString &name) const
int port(int defaultPort) const const
T plugin()
Returns the registered plugin that casts to the template type T.
void setGenericErrorContentTyp(const QString &type)
This class represents a Cutelyst Action.
void setIgnoredNamespaces(const QStringList &namespaces)
Cutelyst Upload handles file upload request
virtual bool setup(Application *app) override
int indexOf(char ch, int from) const const
QString number(int n, int base)
virtual ~CSRFProtection() override
void append(const T &value)
void setDomain(const QString &domain)
QString addressString() const
void stash(const QVariantHash &unite)
Upload * upload(const QString &name) const
Headers headers() const noexcept
static bool checkPassed(Context *c)
QVariantMap config(const QString &entity) const
user configuration for the application
bool isEmpty() const const
const char * constData() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
void setDefaultDetachTo(const QString &actionNameOrPath)
QString header(const QString &key) const
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
static void setValue(Context *c, const QString &key, const QVariant &value)
void setFormFieldName(const QString &fieldName)
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
The Cutelyst namespace holds all public Cutelyst API.
QByteArray mid(int pos, int len) const const
void setCookieName(const QString &cookieName)
void setUseSessions(bool useSessions)
QByteArray & append(char ch)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString ns() const noexcept
void beforeDispatch(Cutelyst::Context *c)
void setCookieHttpOnly(bool httpOnly)
bool contains(const Key &key, const T &value) const const
void setGenericErrorMessage(const QString &message)
QByteArray left(int len) const const
QDateTime currentDateTime()
QByteArray toLatin1() const const
QString mid(int position, int n) const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
void setErrorMsgStashKey(const QString &keyName)
QString fromLatin1(const char *str, int size)
QString cookie(const QString &name) const
The Cutelyst Application.
Engine * engine() const noexcept
CSRFProtection(Application *parent)
QString bodyParam(const QString &key, const QString &defaultValue={}) const
void setBody(QIODevice *body)
QString attribute(const QString &name, const QString &defaultValue={}) const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Action * getActionByPath(const QString &path) const
int compare(const QString &other, Qt::CaseSensitivity cs) const const
static QString getTokenFormField(Context *c)
void setStatus(quint16 status) noexcept
static QByteArray getToken(Context *c)
virtual qint64 size() const override
QString className() const
Dispatcher * dispatcher() const noexcept