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);
455 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
456 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
459 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
"403 Forbidden - CSRF protection check failed");
460 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
461 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
463 " <title>") + title +
464 QStringLiteral(
"</title>\n"
468 QStringLiteral(
"</h1>\n"
469 " <p>") + displayReason +
470 QStringLiteral(
"</p>\n"
479 void CSRFProtectionPrivate::accept(
Context *c)
481 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
482 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
491 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
492 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
496 for (
int i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
497 diff |= _t1[i] ^ _t2[i];
506 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
509 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRFProtection plugin not registered"), c->
translate(
"Cutelyst::CSRFProtection",
"The CSRF protection plugin has not been registered."));
513 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
514 if (!csrfToken.
isNull()) {
515 c->
setStash(CONTEXT_CSRF_COOKIE, csrfToken);
520 if (c->
stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
525 qCDebug(C_CSRFPROTECTION,
"Action \"%s::%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
className()), qPrintable(c->action()->
reverse()));
529 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
531 qCDebug(C_CSRFPROTECTION,
"Namespace \"%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
ns()));
538 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
553 if (c->req()->secure()) {
556 if (Q_UNLIKELY(referer.
isEmpty())) {
557 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - no Referer"), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - no Referer."));
560 const QUrl refererUrl(referer);
561 if (Q_UNLIKELY(!refererUrl.isValid())) {
562 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - Referer is malformed"), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - Referer is malformed."));
565 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"https"))) {
566 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."));
572 const QUrl uri = c->req()->uri();
574 if (!csrf->d_ptr->useSessions) {
575 goodReferer = csrf->d_ptr->cookieDomain;
578 goodReferer = uri.
host();
580 const int serverPort = uri.
port(c->req()->secure() ? 443 : 80);
581 if ((serverPort != 80) && (serverPort != 443)) {
585 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
586 goodHosts.
append(goodReferer);
588 QString refererHost = refererUrl.host();
589 const int refererPort = refererUrl.port(refererUrl.scheme().compare(u
"https") == 0 ? 443 : 80);
590 if ((refererPort != 80) && (refererPort != 443)) {
594 bool refererCheck =
false;
595 for (
int i = 0; i < goodHosts.
size(); ++i) {
603 if (Q_UNLIKELY(!refererCheck)) {
605 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));
613 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
614 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF cookie not set"), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
621 if (c->req()->contentType().
compare(u
"multipart/form-data") == 0) {
623 Upload *upload = c->req()->
upload(csrf->d_ptr->formInputName);
624 if (upload && upload->
size() < 1024 ) {
625 requestCsrfToken = upload->
readAll();
631 if (requestCsrfToken.
isEmpty()) {
632 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName).
toLatin1();
633 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
634 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from HTTP header %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->headerName));
636 qCDebug(C_CSRFPROTECTION,
"Can not get token from HTTP header or form field.");
639 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from form field %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->formInputName));
642 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
644 if (Q_UNLIKELY(!CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
645 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF token missing or incorrect"), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF token missing or incorrect."));
652 CSRFProtectionPrivate::accept(c);
659 if (!c->
stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
660 if (c->
stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
665 if (!c->
stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
669 CSRFProtectionPrivate::setToken(c);
670 c->
setStash(CONTEXT_CSRF_COOKIE_SET,
true);
673 #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