19 #include "csrfprotection_p.h"
21 #include <Cutelyst/Application>
22 #include <Cutelyst/Engine>
23 #include <Cutelyst/Context>
24 #include <Cutelyst/Request>
25 #include <Cutelyst/Response>
26 #include <Cutelyst/Plugins/Session/Session>
27 #include <Cutelyst/Headers>
28 #include <Cutelyst/Action>
29 #include <Cutelyst/Dispatcher>
30 #include <Cutelyst/Controller>
31 #include <Cutelyst/Upload>
33 #include <QLoggingCategory>
34 #include <QNetworkCookie>
41 #define DEFAULT_COOKIE_AGE Q_INT64_C(31449600)
42 #define DEFAULT_COOKIE_NAME "csrftoken"
43 #define DEFAULT_COOKIE_PATH "/"
44 #define DEFAULT_HEADER_NAME "X_CSRFTOKEN"
45 #define DEFAULT_FORM_INPUT_NAME "csrfprotectiontoken"
46 #define CSRF_SECRET_LENGTH 32
47 #define CSRF_TOKEN_LENGTH 2 * CSRF_SECRET_LENGTH
48 #define CSRF_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
49 #define CSRF_SESSION_KEY "_csrftoken"
50 #define CONTEXT_CSRF_COOKIE QStringLiteral("_c_csrfcookie")
51 #define CONTEXT_CSRF_COOKIE_USED QStringLiteral("_c_csrfcookieused")
52 #define CONTEXT_CSRF_COOKIE_NEEDS_RESET QStringLiteral("_c_csrfcookieneedsreset")
53 #define CONTEXT_CSRF_PROCESSING_DONE QStringLiteral("_c_csrfprocessingdone")
54 #define CONTEXT_CSRF_COOKIE_SET QStringLiteral("_c_csrfcookieset")
55 #define CONTEXT_CSRF_CHECK_PASSED QStringLiteral("_c_csrfcheckpassed")
57 Q_LOGGING_CATEGORY(C_CSRFPROTECTION,
"cutelyst.plugin.csrfprotection", QtWarningMsg)
64 const QStringList CSRFProtectionPrivate::secureMethods =
QStringList({QStringLiteral(
"GET"), QStringLiteral(
"HEAD"), QStringLiteral(
"OPTIONS"), QStringLiteral(
"TRACE")});
67 , d_ptr(new CSRFProtectionPrivate)
83 const QVariantMap config = app->
engine()->
config(QStringLiteral(
"Cutelyst_CSRFProtection_Plugin"));
85 d->cookieAge = config.value(QStringLiteral(
"cookie_age"), DEFAULT_COOKIE_AGE).value<qint64>();
86 if (d->cookieAge <= 0) {
87 d->cookieAge = DEFAULT_COOKIE_AGE;
89 d->cookieDomain = config.value(QStringLiteral(
"cookie_domain")).toString();
90 if (d->cookieName.isEmpty()) {
91 d->cookieName = QStringLiteral(DEFAULT_COOKIE_NAME);
93 d->cookiePath = QStringLiteral(DEFAULT_COOKIE_PATH);
94 d->cookieSecure = config.value(QStringLiteral(
"cookie_secure"),
false).toBool();
95 if (d->headerName.isEmpty()) {
96 d->headerName = QStringLiteral(DEFAULT_HEADER_NAME);
99 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
104 if (d->formInputName.isEmpty()) {
105 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
107 d->logFailedIp = config.value(QStringLiteral(
"log_failed_ip"),
false).toBool();
108 if (d->errorMsgStashKey.isEmpty()) {
109 d->errorMsgStashKey = QStringLiteral(
"error_msg");
117 d->beforeDispatch(c);
126 d->defaultDetachTo = actionNameOrPath;
133 d->formInputName = fieldName;
135 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
143 d->errorMsgStashKey = keyName;
145 d->errorMsgStashKey = QStringLiteral(
"error_msg");
152 d->ignoredNamespaces = namespaces;
158 d->useSessions = useSessions;
164 d->cookieHttpOnly = httpOnly;
170 d->cookieName = cookieName;
176 d->headerName = headerName;
182 d->genericErrorMessage = message;
188 d->genericContentType = type;
195 const QByteArray contextCookie = c->
stash(CONTEXT_CSRF_COOKIE).toByteArray();
198 secret = CSRFProtectionPrivate::getNewCsrfString();
199 token = CSRFProtectionPrivate::saltCipherSecret(secret);
200 c->
setStash(CONTEXT_CSRF_COOKIE, token);
202 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
203 token = CSRFProtectionPrivate::saltCipherSecret(secret);
206 c->
setStash(CONTEXT_CSRF_COOKIE_USED,
true);
216 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
227 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
230 return c->
stash(CONTEXT_CSRF_CHECK_PASSED).toBool();
245 QByteArray CSRFProtectionPrivate::getNewCsrfString()
249 while (csrfString.
size() < CSRF_SECRET_LENGTH) {
253 csrfString.
resize(CSRF_SECRET_LENGTH);
266 salted.
reserve(CSRF_TOKEN_LENGTH);
268 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
269 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
270 std::vector<std::pair<int,int>> pairs;
271 pairs.reserve(std::min(secret.
size(), salt.
size()));
272 for (
int i = 0; i < std::min(secret.
size(), salt.
size()); ++i) {
273 pairs.push_back(std::make_pair(chars.
indexOf(secret.
at(i)), chars.
indexOf(salt.
at(i))));
277 cipher.
reserve(CSRF_SECRET_LENGTH);
278 for (std::size_t i = 0; i < pairs.size(); ++i) {
279 const std::pair<int,int> p = pairs.at(i);
280 cipher.
append(chars[(p.first + p.second) % chars.
size()]);
283 salted = salt + cipher;
297 secret.
reserve(CSRF_SECRET_LENGTH);
302 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
303 std::vector<std::pair<int,int>> pairs;
304 pairs.reserve(std::min(salt.
size(), _token.
size()));
305 for (
int i = 0; i < std::min(salt.
size(), _token.
size()); ++i) {
306 pairs.push_back(std::make_pair(chars.
indexOf(_token.
at(i)), chars.
indexOf(salt.
at(i))));
310 for (std::size_t i = 0; i < pairs.size(); ++i) {
311 const std::pair<int,int> p = pairs.at(i);
312 int idx = p.first - p.second;
314 idx = chars.
size() + idx;
327 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
329 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
342 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe)) {
343 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
344 }
else if (token.
size() != CSRF_TOKEN_LENGTH) {
345 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
362 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
366 if (csrf->d_ptr->useSessions) {
375 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
376 if (token != cookieToken) {
377 c->
setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET,
true);
381 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from %s.", token.
constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
390 void CSRFProtectionPrivate::setToken(
Context *c)
393 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
397 if (csrf->d_ptr->useSessions) {
400 QNetworkCookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
401 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
402 cookie.setDomain(csrf->d_ptr->cookieDomain);
405 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
406 cookie.setPath(csrf->d_ptr->cookiePath);
407 cookie.setSecure(csrf->d_ptr->cookieSecure);
412 qCDebug(C_CSRFPROTECTION,
"Set token \"%s\" to %s.", c->
stash(CONTEXT_CSRF_COOKIE).toByteArray().constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
420 void CSRFProtectionPrivate::reject(
Context *c,
const QString &logReason,
const QString &displayReason)
422 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
false);
425 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
429 qCWarning(C_CSRFPROTECTION,
"Forbidden: (%s): /%s [%s]", qPrintable(logReason), qPrintable(c->req()->path()), csrf->d_ptr->logFailedIp ? qPrintable(c->req()->
addressString()) :
"IP logging disabled");
432 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
434 QString detachToCsrf = c->action()->
attribute(QStringLiteral(
"CSRFDetachTo"));
436 detachToCsrf = csrf->d_ptr->defaultDetachTo;
439 Action *detachToAction =
nullptr;
442 detachToAction = c->controller()->
actionFor(detachToCsrf);
443 if (!detachToAction) {
446 if (!detachToAction) {
447 qCWarning(C_CSRFPROTECTION,
"Can not find action for \"%s\" to detach to.", qPrintable(detachToCsrf));
451 if (detachToAction) {
452 c->
detach(detachToAction);
454 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
455 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
458 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
"403 Forbidden - CSRF protection check failed");
459 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
460 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
462 " <title>") + title +
463 QStringLiteral(
"</title>\n"
467 QStringLiteral(
"</h1>\n"
468 " <p>") + displayReason +
469 QStringLiteral(
"</p>\n"
478 void CSRFProtectionPrivate::accept(
Context *c)
480 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
481 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
490 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
491 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
495 for (
int i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
496 diff |= _t1[i] ^ _t2[i];
505 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
508 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRFProtection plugin not registered"), c->
translate(
"Cutelyst::CSRFProtection",
"The CSRF protection plugin has not been registered."));
512 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
513 if (!csrfToken.
isNull()) {
514 c->
setStash(CONTEXT_CSRF_COOKIE, csrfToken);
519 if (c->
stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
524 qCDebug(C_CSRFPROTECTION,
"Action \"%s::%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
className()), qPrintable(c->action()->
reverse()));
528 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
530 qCDebug(C_CSRFPROTECTION,
"Namespace \"%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
ns()));
537 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
552 if (c->req()->secure()) {
555 if (Q_UNLIKELY(referer.
isEmpty())) {
556 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - no Referer."), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - no Referer."));
559 const QUrl refererUrl(referer);
560 if (Q_UNLIKELY(!refererUrl.isValid())) {
561 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - Referer is malformed."), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - Referer is malformed."));
564 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"https"))) {
565 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."));
571 const QUrl uri = c->req()->uri();
573 if (!csrf->d_ptr->useSessions) {
574 goodReferer = csrf->d_ptr->cookieDomain;
577 goodReferer = uri.
host();
579 const int serverPort = uri.
port(c->req()->secure() ? 443 : 80);
580 if ((serverPort != 80) && (serverPort != 443)) {
584 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
585 goodHosts.
append(goodReferer);
587 QString refererHost = refererUrl.host();
588 const int refererPort = refererUrl.port(refererUrl.scheme() ==
QLatin1String(
"https") ? 443 : 80);
589 if ((refererPort != 80) && (refererPort != 443)) {
593 bool refererCheck =
false;
594 for (
int i = 0; i < goodHosts.
size(); ++i) {
602 if (Q_UNLIKELY(!refererCheck)) {
604 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));
612 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
613 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF cookie not set."), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
620 if (c->req()->contentType() ==
QLatin1String(
"multipart/form-data")) {
622 Upload *upload = c->req()->
upload(csrf->d_ptr->formInputName);
623 if (upload && upload->
size() < 1024 ) {
624 requestCsrfToken = upload->
readAll();
630 if (requestCsrfToken.
isEmpty()) {
631 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName).
toLatin1();
632 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
633 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from HTTP header %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->headerName));
635 qCDebug(C_CSRFPROTECTION,
"Can not get token from HTTP header or form field.");
638 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from form field %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->formInputName));
641 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
643 if (Q_UNLIKELY(!CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
644 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF token missing or incorrect."), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF token missing or incorrect."));
651 CSRFProtectionPrivate::accept(c);
658 if (!c->
stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
659 if (c->
stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
664 if (!c->
stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
668 CSRFProtectionPrivate::setToken(c);
669 c->
setStash(CONTEXT_CSRF_COOKIE_SET,
true);
672 #include "moc_csrfprotection.cpp"
This class represents a Cutelyst Action.
QString attribute(const QString &name, const QString &defaultValue=QString()) const
ParamsMultiMap attributes() const
QString className() const
The Cutelyst Application.
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)
Dispatcher * dispatcher() const
void detach(Action *action=nullptr)
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
void setStash(const QString &key, const QVariant &value)
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
QString cookie(const QString &name) const
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Upload * upload(const QString &name) const
void setBody(QIODevice *body)
void setStatus(quint16 status)
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
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