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 d->trustedOrigins = config.value(QStringLiteral(
"trusted_origins")).toString().split(u
',',
Qt::SkipEmptyParts);
87 if (d->formInputName.isEmpty()) {
88 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
90 d->logFailedIp = config.value(QStringLiteral(
"log_failed_ip"),
false).toBool();
91 if (d->errorMsgStashKey.isEmpty()) {
92 d->errorMsgStashKey = QStringLiteral(
"error_msg");
100 d->beforeDispatch(c);
109 d->defaultDetachTo = actionNameOrPath;
116 d->formInputName = fieldName;
118 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
126 d->errorMsgStashKey = keyName;
128 d->errorMsgStashKey = QStringLiteral(
"error_msg");
135 d->ignoredNamespaces = namespaces;
141 d->useSessions = useSessions;
147 d->cookieHttpOnly = httpOnly;
153 d->cookieName = cookieName;
159 d->headerName = headerName;
165 d->genericErrorMessage = message;
171 d->genericContentType = type;
178 const QByteArray contextCookie = c->
stash(CONTEXT_CSRF_COOKIE).toByteArray();
181 secret = CSRFProtectionPrivate::getNewCsrfString();
182 token = CSRFProtectionPrivate::saltCipherSecret(secret);
183 c->
setStash(CONTEXT_CSRF_COOKIE, token);
185 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
186 token = CSRFProtectionPrivate::saltCipherSecret(secret);
189 c->
setStash(CONTEXT_CSRF_COOKIE_USED,
true);
199 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
210 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
213 return c->
stash(CONTEXT_CSRF_CHECK_PASSED).toBool();
228 QByteArray CSRFProtectionPrivate::getNewCsrfString()
232 while (csrfString.
size() < CSRF_SECRET_LENGTH) {
236 csrfString.
resize(CSRF_SECRET_LENGTH);
249 salted.
reserve(CSRF_TOKEN_LENGTH);
251 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
252 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
253 std::vector<std::pair<int,int>> pairs;
254 pairs.reserve(std::min(secret.
size(), salt.
size()));
255 for (
int i = 0; i < std::min(secret.
size(), salt.
size()); ++i) {
256 pairs.push_back(std::make_pair(chars.
indexOf(secret.
at(i)), chars.
indexOf(salt.
at(i))));
260 cipher.
reserve(CSRF_SECRET_LENGTH);
261 for (std::size_t i = 0; i < pairs.size(); ++i) {
262 const std::pair<int,int> p = pairs.at(i);
263 cipher.
append(chars[(p.first + p.second) % chars.
size()]);
266 salted = salt + cipher;
280 secret.
reserve(CSRF_SECRET_LENGTH);
285 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
286 std::vector<std::pair<int,int>> pairs;
287 pairs.reserve(std::min(salt.
size(), _token.
size()));
288 for (
int i = 0; i < std::min(salt.
size(), _token.
size()); ++i) {
289 pairs.push_back(std::make_pair(chars.
indexOf(_token.
at(i)), chars.
indexOf(salt.
at(i))));
293 for (std::size_t i = 0; i < pairs.size(); ++i) {
294 const std::pair<int,int> p = pairs.at(i);
295 int idx = p.first - p.second;
297 idx = chars.
size() + idx;
310 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
312 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
325 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe)) {
326 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
327 }
else if (token.
size() != CSRF_TOKEN_LENGTH) {
328 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
345 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
349 if (csrf->d_ptr->useSessions) {
358 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
359 if (token != cookieToken) {
360 c->
setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET,
true);
364 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from %s.", token.
constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
373 void CSRFProtectionPrivate::setToken(
Context *c)
376 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
380 if (csrf->d_ptr->useSessions) {
383 QNetworkCookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
384 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
385 cookie.setDomain(csrf->d_ptr->cookieDomain);
388 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
389 cookie.setPath(csrf->d_ptr->cookiePath);
390 cookie.setSecure(csrf->d_ptr->cookieSecure);
395 qCDebug(C_CSRFPROTECTION,
"Set token \"%s\" to %s.", c->
stash(CONTEXT_CSRF_COOKIE).toByteArray().constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
403 void CSRFProtectionPrivate::reject(
Context *c,
const QString &logReason,
const QString &displayReason)
405 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
false);
408 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
412 qCWarning(C_CSRFPROTECTION,
"Forbidden: (%s): /%s [%s]", qPrintable(logReason), qPrintable(c->req()->path()), csrf->d_ptr->logFailedIp ? qPrintable(c->req()->
addressString()) :
"IP logging disabled");
415 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
417 QString detachToCsrf = c->action()->
attribute(QStringLiteral(
"CSRFDetachTo"));
419 detachToCsrf = csrf->d_ptr->defaultDetachTo;
422 Action *detachToAction =
nullptr;
425 detachToAction = c->controller()->
actionFor(detachToCsrf);
426 if (!detachToAction) {
429 if (!detachToAction) {
430 qCWarning(C_CSRFPROTECTION,
"Can not find action for \"%s\" to detach to.", qPrintable(detachToCsrf));
434 if (detachToAction) {
435 c->
detach(detachToAction);
438 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
439 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
442 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
"403 Forbidden - CSRF protection check failed");
443 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
444 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
446 " <title>") + title +
447 QStringLiteral(
"</title>\n"
451 QStringLiteral(
"</h1>\n"
452 " <p>") + displayReason +
453 QStringLiteral(
"</p>\n"
462 void CSRFProtectionPrivate::accept(
Context *c)
464 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
465 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
474 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
475 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
479 for (
int i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
480 diff |= _t1[i] ^ _t2[i];
489 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
492 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRFProtection plugin not registered"), c->
translate(
"Cutelyst::CSRFProtection",
"The CSRF protection plugin has not been registered."));
496 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
497 if (!csrfToken.
isNull()) {
498 c->
setStash(CONTEXT_CSRF_COOKIE, csrfToken);
503 if (c->
stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
508 qCDebug(C_CSRFPROTECTION,
"Action \"%s::%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
className()), qPrintable(c->action()->
reverse()));
512 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
514 qCDebug(C_CSRFPROTECTION,
"Namespace \"%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
ns()));
521 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
536 if (c->req()->secure()) {
539 if (Q_UNLIKELY(referer.
isEmpty())) {
540 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - no Referer"), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - no Referer."));
543 const QUrl refererUrl(referer);
544 if (Q_UNLIKELY(!refererUrl.isValid())) {
545 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - Referer is malformed"), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - Referer is malformed."));
548 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"https"))) {
549 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."));
555 const QUrl uri = c->req()->uri();
557 if (!csrf->d_ptr->useSessions) {
558 goodReferer = csrf->d_ptr->cookieDomain;
561 goodReferer = uri.
host();
563 const int serverPort = uri.
port(c->req()->secure() ? 443 : 80);
564 if ((serverPort != 80) && (serverPort != 443)) {
568 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
569 goodHosts.
append(goodReferer);
571 QString refererHost = refererUrl.host();
572 const int refererPort = refererUrl.port(refererUrl.scheme().compare(u
"https") == 0 ? 443 : 80);
573 if ((refererPort != 80) && (refererPort != 443)) {
577 bool refererCheck =
false;
578 for (
int i = 0; i < goodHosts.
size(); ++i) {
580 if ((host.
startsWith(u
'.') && (refererHost.
endsWith(host) || (refererHost == host.
mid(1)))) || host == refererHost) {
586 if (Q_UNLIKELY(!refererCheck)) {
588 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));
596 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
597 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF cookie not set"), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
604 if (c->req()->contentType().
compare(u
"multipart/form-data") == 0) {
606 Upload *upload = c->req()->
upload(csrf->d_ptr->formInputName);
607 if (upload && upload->
size() < 1024 ) {
608 requestCsrfToken = upload->
readAll();
614 if (requestCsrfToken.
isEmpty()) {
615 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName).
toLatin1();
616 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
617 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from HTTP header %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->headerName));
619 qCDebug(C_CSRFPROTECTION,
"Can not get token from HTTP header or form field.");
622 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from form field %s.", requestCsrfToken.
constData(), qPrintable(csrf->d_ptr->formInputName));
625 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
627 if (Q_UNLIKELY(!CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
628 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF token missing or incorrect"), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF token missing or incorrect."));
635 CSRFProtectionPrivate::accept(c);
642 if (!c->
stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
643 if (c->
stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
648 if (!c->
stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
652 CSRFProtectionPrivate::setToken(c);
653 c->
setStash(CONTEXT_CSRF_COOKIE_SET,
true);
656 #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