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)
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))
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) {
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"
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