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> 21 #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_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);
86 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) 87 d->trustedOrigins = config.value(QStringLiteral(
"trusted_origins")).toString().split(
QLatin1Char(
','), Qt::SkipEmptyParts);
89 d->trustedOrigins = config.value(QStringLiteral(
"trusted_origins")).toString().split(
QLatin1Char(
','), QString::SkipEmptyParts);
91 if (d->formInputName.isEmpty()) {
92 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
94 d->logFailedIp = config.value(QStringLiteral(
"log_failed_ip"),
false).toBool();
95 if (d->errorMsgStashKey.isEmpty()) {
96 d->errorMsgStashKey = QStringLiteral(
"error_msg");
104 d->beforeDispatch(c);
113 d->defaultDetachTo = actionNameOrPath;
120 d->formInputName = fieldName;
122 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
130 d->errorMsgStashKey = keyName;
132 d->errorMsgStashKey = QStringLiteral(
"error_msg");
139 d->ignoredNamespaces = namespaces;
145 d->useSessions = useSessions;
151 d->cookieHttpOnly = httpOnly;
157 d->cookieName = cookieName;
163 d->headerName = headerName;
169 d->genericErrorMessage = message;
175 d->genericContentType = type;
182 const QByteArray contextCookie = c->
stash(CONTEXT_CSRF_COOKIE).toByteArray();
185 secret = CSRFProtectionPrivate::getNewCsrfString();
186 token = CSRFProtectionPrivate::saltCipherSecret(secret);
187 c->
setStash(CONTEXT_CSRF_COOKIE, token);
189 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
190 token = CSRFProtectionPrivate::saltCipherSecret(secret);
193 c->
setStash(CONTEXT_CSRF_COOKIE_USED,
true);
203 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
214 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
217 return c->
stash(CONTEXT_CSRF_CHECK_PASSED).toBool();
232 QByteArray CSRFProtectionPrivate::getNewCsrfString()
236 while (csrfString.
size() < CSRF_SECRET_LENGTH) {
237 csrfString.
append(
QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
240 csrfString.
resize(CSRF_SECRET_LENGTH);
253 salted.
reserve(CSRF_TOKEN_LENGTH);
255 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
256 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
257 std::vector<std::pair<int,int>> pairs;
258 pairs.reserve(std::min(secret.
size(), salt.
size()));
259 for (
int i = 0; i < std::min(secret.
size(), salt.
size()); ++i) {
260 pairs.push_back(std::make_pair(chars.
indexOf(secret.
at(i)), chars.
indexOf(salt.
at(i))));
264 cipher.
reserve(CSRF_SECRET_LENGTH);
265 for (std::size_t i = 0; i < pairs.size(); ++i) {
266 const std::pair<int,int> p = pairs.at(i);
267 cipher.
append(chars[(p.first + p.second) % chars.
size()]);
270 salted = salt + cipher;
284 secret.
reserve(CSRF_SECRET_LENGTH);
289 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
290 std::vector<std::pair<int,int>> pairs;
291 pairs.reserve(std::min(salt.
size(), _token.
size()));
292 for (
int i = 0; i < std::min(salt.
size(), _token.
size()); ++i) {
293 pairs.push_back(std::make_pair(chars.
indexOf(_token.
at(i)), chars.
indexOf(salt.
at(i))));
297 for (std::size_t i = 0; i < pairs.size(); ++i) {
298 const std::pair<int,int> p = pairs.at(i);
299 int idx = p.first - p.second;
301 idx = chars.
size() + idx;
314 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
316 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
329 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe)) {
330 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
331 }
else if (token.
size() != CSRF_TOKEN_LENGTH) {
332 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
349 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
353 if (csrf->d_ptr->useSessions) {
362 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
363 if (token != cookieToken) {
364 c->
setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET,
true);
368 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from %s.", token.
constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
377 void CSRFProtectionPrivate::setToken(
Context *c)
380 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
384 if (csrf->d_ptr->useSessions) {
387 QNetworkCookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
388 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
389 cookie.
setDomain(csrf->d_ptr->cookieDomain);
392 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
393 cookie.setPath(csrf->d_ptr->cookiePath);
394 cookie.setSecure(csrf->d_ptr->cookieSecure);
399 qCDebug(C_CSRFPROTECTION,
"Set token \"%s\" to %s.", c->
stash(CONTEXT_CSRF_COOKIE).toByteArray().constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
407 void CSRFProtectionPrivate::reject(
Context *c,
const QString &logReason,
const QString &displayReason)
409 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
false);
412 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
416 qCWarning(C_CSRFPROTECTION,
"Forbidden: (%s): /%s [%s]", qPrintable(logReason), qPrintable(c->req()->path()), csrf->d_ptr->logFailedIp ? qPrintable(c->req()->
addressString()) :
"IP logging disabled");
419 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
421 QString detachToCsrf = c->action()->
attribute(QStringLiteral(
"CSRFDetachTo"));
423 detachToCsrf = csrf->d_ptr->defaultDetachTo;
426 Action *detachToAction =
nullptr;
429 detachToAction = c->controller()->
actionFor(detachToCsrf);
430 if (!detachToAction) {
433 if (!detachToAction) {
434 qCWarning(C_CSRFPROTECTION,
"Can not find action for \"%s\" to detach to.", qPrintable(detachToCsrf));
438 if (detachToAction) {
439 c->
detach(detachToAction);
442 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
443 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
446 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
"403 Forbidden - CSRF protection check failed");
447 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n" 448 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" 450 " <title>") + title +
451 QStringLiteral(
"</title>\n" 455 QStringLiteral(
"</h1>\n" 456 " <p>") + displayReason +
457 QStringLiteral(
"</p>\n" 466 void CSRFProtectionPrivate::accept(
Context *c)
468 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
469 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
478 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
479 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
483 for (
int i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
484 diff |= _t1[i] ^ _t2[i];
493 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
496 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRFProtection plugin not registered"), c->
translate(
"Cutelyst::CSRFProtection",
"The CSRF protection plugin has not been registered."));
500 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
501 if (!csrfToken.
isNull()) {
502 c->
setStash(CONTEXT_CSRF_COOKIE, csrfToken);
507 if (c->
stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
512 qCDebug(C_CSRFPROTECTION,
"Action \"%s::%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
className()), qPrintable(c->action()->
reverse()));
516 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
518 qCDebug(C_CSRFPROTECTION,
"Namespace \"%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
ns()));
525 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
540 if (c->req()->secure()) {
543 if (Q_UNLIKELY(referer.
isEmpty())) {
544 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - no Referer"), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - no Referer."));
547 const QUrl refererUrl(referer);
548 if (Q_UNLIKELY(!refererUrl.isValid())) {
549 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - Referer is malformed"), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - Referer is malformed."));
552 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"https"))) {
553 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."));
559 const QUrl uri = c->req()->uri();
561 if (!csrf->d_ptr->useSessions) {
562 goodReferer = csrf->d_ptr->cookieDomain;
565 goodReferer = uri.
host();
567 const int serverPort = uri.
port(c->req()->secure() ? 443 : 80);
568 if ((serverPort != 80) && (serverPort != 443)) {
572 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
573 goodHosts.
append(goodReferer);
575 QString refererHost = refererUrl.host();
576 const int refererPort = refererUrl.port(refererUrl.scheme().compare(u
"https") == 0 ? 443 : 80);
577 if ((refererPort != 80) && (refererPort != 443)) {
581 bool refererCheck =
false;
582 for (
int i = 0; i < goodHosts.
size(); ++i) {
590 if (Q_UNLIKELY(!refererCheck)) {
592 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));
600 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
601 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF cookie not set"), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
608 if (c->req()->contentType().
compare(u
"multipart/form-data") == 0) {
610 Upload *upload = c->req()->
upload(csrf->d_ptr->formInputName);
611 if (upload && upload->
size() < 1024 ) {
612 requestCsrfToken = upload->
readAll();
618 if (requestCsrfToken.
isEmpty()) {
619 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName).
toLatin1();
620 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
621 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from HTTP header %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->headerName));
623 qCDebug(C_CSRFPROTECTION,
"Can not get token from HTTP header or form field.");
626 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from form field %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->formInputName));
629 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
631 if (Q_UNLIKELY(!CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
632 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF token missing or incorrect"), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF token missing or incorrect."));
639 CSRFProtectionPrivate::accept(c);
646 if (!c->
stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
647 if (c->
stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
652 if (!c->
stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
656 CSRFProtectionPrivate::setToken(c);
657 c->
setStash(CONTEXT_CSRF_COOKIE_SET,
true);
660 #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
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
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) 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