6 #include "csrfprotection_p.h"
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>
20 #include <QLoggingCategory>
27 #define DEFAULT_COOKIE_AGE Q_INT64_C(31449600)
28 #define DEFAULT_COOKIE_NAME "csrftoken"
29 #define DEFAULT_COOKIE_PATH "/"
30 #define DEFAULT_COOKIE_SAMESITE "strict"
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")
44 Q_LOGGING_CATEGORY(C_CSRFPROTECTION,
"cutelyst.plugin.csrfprotection", QtWarningMsg)
51 const QStringList CSRFProtectionPrivate::secureMethods =
QStringList({QStringLiteral(
"GET"), QStringLiteral(
"HEAD"), QStringLiteral(
"OPTIONS"), QStringLiteral(
"TRACE")});
54 , d_ptr(new CSRFProtectionPrivate)
70 const QVariantMap config = app->
engine()->
config(QStringLiteral(
"Cutelyst_CSRFProtection_Plugin"));
72 d->cookieAge = config.value(QStringLiteral(
"cookie_age"), DEFAULT_COOKIE_AGE).value<qint64>();
73 if (d->cookieAge <= 0) {
74 d->cookieAge = DEFAULT_COOKIE_AGE;
76 d->cookieDomain = config.value(QStringLiteral(
"cookie_domain")).toString();
77 if (d->cookieName.isEmpty()) {
78 d->cookieName = QStringLiteral(DEFAULT_COOKIE_NAME);
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);
85 const QString _sameSite = config.value(
QLatin1String(
"cookie_same_site"), QStringLiteral(
"strict")).toString();
86 #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0)
88 d->cookieSameSite = QNetworkCookie::SameSite::Default;
90 d->cookieSameSite = QNetworkCookie::SameSite::None;
92 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
94 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
108 d->trustedOrigins = config.value(QStringLiteral(
"trusted_origins")).toString().split(u
',',
Qt::SkipEmptyParts);
109 if (d->formInputName.isEmpty()) {
110 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
112 d->logFailedIp = config.value(QStringLiteral(
"log_failed_ip"),
false).toBool();
113 if (d->errorMsgStashKey.isEmpty()) {
114 d->errorMsgStashKey = QStringLiteral(
"error_msg");
122 d->beforeDispatch(c);
131 d->defaultDetachTo = actionNameOrPath;
138 d->formInputName = fieldName;
140 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
148 d->errorMsgStashKey = keyName;
150 d->errorMsgStashKey = QStringLiteral(
"error_msg");
157 d->ignoredNamespaces = namespaces;
163 d->useSessions = useSessions;
169 d->cookieHttpOnly = httpOnly;
175 d->cookieName = cookieName;
181 d->headerName = headerName;
187 d->genericErrorMessage = message;
193 d->genericContentType = type;
200 const QByteArray contextCookie = c->
stash(CONTEXT_CSRF_COOKIE).toByteArray();
203 secret = CSRFProtectionPrivate::getNewCsrfString();
204 token = CSRFProtectionPrivate::saltCipherSecret(secret);
205 c->
setStash(CONTEXT_CSRF_COOKIE, token);
207 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
208 token = CSRFProtectionPrivate::saltCipherSecret(secret);
211 c->
setStash(CONTEXT_CSRF_COOKIE_USED,
true);
221 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
232 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
235 return c->
stash(CONTEXT_CSRF_CHECK_PASSED).toBool();
250 QByteArray CSRFProtectionPrivate::getNewCsrfString()
254 while (csrfString.
size() < CSRF_SECRET_LENGTH) {
258 csrfString.
resize(CSRF_SECRET_LENGTH);
271 salted.
reserve(CSRF_TOKEN_LENGTH);
273 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
274 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
275 std::vector<std::pair<int,int>> pairs;
276 pairs.reserve(std::min(secret.
size(), salt.
size()));
277 for (
int i = 0; i < std::min(secret.
size(), salt.
size()); ++i) {
278 pairs.push_back(std::make_pair(chars.
indexOf(secret.
at(i)), chars.
indexOf(salt.
at(i))));
282 cipher.
reserve(CSRF_SECRET_LENGTH);
283 for (std::size_t i = 0; i < pairs.size(); ++i) {
284 const std::pair<int,int> p = pairs.at(i);
285 cipher.
append(chars[(p.first + p.second) % chars.
size()]);
288 salted = salt + cipher;
302 secret.
reserve(CSRF_SECRET_LENGTH);
307 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
308 std::vector<std::pair<int,int>> pairs;
309 pairs.reserve(std::min(salt.
size(), _token.
size()));
310 for (
int i = 0; i < std::min(salt.
size(), _token.
size()); ++i) {
311 pairs.push_back(std::make_pair(chars.
indexOf(_token.
at(i)), chars.
indexOf(salt.
at(i))));
315 for (std::size_t i = 0; i < pairs.size(); ++i) {
316 const std::pair<int,int> p = pairs.at(i);
317 int idx = p.first - p.second;
319 idx = chars.
size() + idx;
332 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
334 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
347 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe)) {
348 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
349 }
else if (token.
size() != CSRF_TOKEN_LENGTH) {
350 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
367 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
371 if (csrf->d_ptr->useSessions) {
380 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
381 if (token != cookieToken) {
382 c->
setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET,
true);
386 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from %s.", token.
constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
395 void CSRFProtectionPrivate::setToken(
Context *c)
398 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
402 if (csrf->d_ptr->useSessions) {
405 #if (QT_VERSION >= QT_VERSION_CHECK(6, 1, 0))
406 QNetworkCookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
408 Cookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
410 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
411 cookie.setDomain(csrf->d_ptr->cookieDomain);
414 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
415 cookie.setPath(csrf->d_ptr->cookiePath);
416 cookie.setSecure(csrf->d_ptr->cookieSecure);
417 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
418 #if (QT_VERSION >= QT_VERSION_CHECK(6, 1, 0))
421 c->
res()->setCuteCookie(cookie);
426 qCDebug(C_CSRFPROTECTION,
"Set token \"%s\" to %s.", c->
stash(CONTEXT_CSRF_COOKIE).toByteArray().constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
434 void CSRFProtectionPrivate::reject(
Context *c,
const QString &logReason,
const QString &displayReason)
436 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
false);
439 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
443 qCWarning(C_CSRFPROTECTION,
"Forbidden: (%s): /%s [%s]", qPrintable(logReason), qPrintable(c->req()->path()), csrf->d_ptr->logFailedIp ? qPrintable(c->req()->
addressString()) :
"IP logging disabled");
446 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
448 QString detachToCsrf = c->action()->
attribute(QStringLiteral(
"CSRFDetachTo"));
450 detachToCsrf = csrf->d_ptr->defaultDetachTo;
453 Action *detachToAction =
nullptr;
456 detachToAction = c->controller()->
actionFor(detachToCsrf);
457 if (!detachToAction) {
460 if (!detachToAction) {
461 qCWarning(C_CSRFPROTECTION,
"Can not find action for \"%s\" to detach to.", qPrintable(detachToCsrf));
465 if (detachToAction) {
466 c->
detach(detachToAction);
469 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
470 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
473 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
"403 Forbidden - CSRF protection check failed");
474 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
475 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
477 " <title>") + title +
478 QStringLiteral(
"</title>\n"
482 QStringLiteral(
"</h1>\n"
483 " <p>") + displayReason +
484 QStringLiteral(
"</p>\n"
493 void CSRFProtectionPrivate::accept(
Context *c)
495 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
496 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
505 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
506 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
510 for (
int i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
511 diff |= _t1[i] ^ _t2[i];
520 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
523 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRFProtection plugin not registered"), c->
translate(
"Cutelyst::CSRFProtection",
"The CSRF protection plugin has not been registered."));
527 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
528 if (!csrfToken.
isNull()) {
529 c->
setStash(CONTEXT_CSRF_COOKIE, csrfToken);
534 if (c->
stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
539 qCDebug(C_CSRFPROTECTION,
"Action \"%s::%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
className()), qPrintable(c->action()->
reverse()));
543 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
545 qCDebug(C_CSRFPROTECTION,
"Namespace \"%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
ns()));
552 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
567 if (c->req()->secure()) {
570 if (Q_UNLIKELY(referer.
isEmpty())) {
571 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - no Referer"), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - no Referer."));
574 const QUrl refererUrl(referer);
575 if (Q_UNLIKELY(!refererUrl.isValid())) {
576 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - Referer is malformed"), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - Referer is malformed."));
579 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"https"))) {
580 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."));
586 const QUrl uri = c->req()->uri();
588 if (!csrf->d_ptr->useSessions) {
589 goodReferer = csrf->d_ptr->cookieDomain;
592 goodReferer = uri.
host();
594 const int serverPort = uri.
port(c->req()->secure() ? 443 : 80);
595 if ((serverPort != 80) && (serverPort != 443)) {
599 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
600 goodHosts.
append(goodReferer);
602 QString refererHost = refererUrl.host();
603 const int refererPort = refererUrl.port(refererUrl.scheme().compare(u
"https") == 0 ? 443 : 80);
604 if ((refererPort != 80) && (refererPort != 443)) {
608 bool refererCheck =
false;
609 for (
int i = 0; i < goodHosts.
size(); ++i) {
611 if ((host.
startsWith(u
'.') && (refererHost.
endsWith(host) || (refererHost == host.
mid(1)))) || host == refererHost) {
617 if (Q_UNLIKELY(!refererCheck)) {
619 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));
627 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
628 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF cookie not set"), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
635 if (c->req()->contentType().
compare(u
"multipart/form-data") == 0) {
637 Upload *upload = c->req()->
upload(csrf->d_ptr->formInputName);
638 if (upload && upload->
size() < 1024 ) {
639 requestCsrfToken = upload->
readAll();
645 if (requestCsrfToken.
isEmpty()) {
646 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName).
toLatin1();
647 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
648 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from HTTP header %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->headerName));
650 qCDebug(C_CSRFPROTECTION,
"Can not get token from HTTP header or form field.");
653 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from form field %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->formInputName));
656 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
658 if (Q_UNLIKELY(!CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
659 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF token missing or incorrect"), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF token missing or incorrect."));
666 CSRFProtectionPrivate::accept(c);
673 if (!c->
stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
674 if (c->
stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
679 if (!c->
stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
683 CSRFProtectionPrivate::setToken(c);
684 c->
setStash(CONTEXT_CSRF_COOKIE_SET,
true);
687 #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
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
void setStatus(quint16 status) noexcept
Headers & headers() 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)
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(QUrl::ComponentFormattingOptions options) const const
int port(int defaultPort) const const
QByteArray toByteArray() const const