6 #include "csrfprotection_p.h" 8 #include <Cutelyst/Action> 9 #include <Cutelyst/Application> 10 #include <Cutelyst/Context> 11 #include <Cutelyst/Controller> 12 #include <Cutelyst/Dispatcher> 13 #include <Cutelyst/Engine> 14 #include <Cutelyst/Headers> 15 #include <Cutelyst/Plugins/Session/Session> 16 #include <Cutelyst/Request> 17 #include <Cutelyst/Response> 18 #include <Cutelyst/Upload> 19 #include <Cutelyst/utils.h> 24 #include <QLoggingCategory> 25 #include <QNetworkCookie> 29 Q_LOGGING_CATEGORY(C_CSRFPROTECTION,
"cutelyst.plugin.csrfprotection", QtWarningMsg)
43 const QByteArray CSRFProtectionPrivate::allowedChars{
44 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"_qba};
45 const QString CSRFProtectionPrivate::sessionKey{u
"_csrftoken"_qs};
46 const QString CSRFProtectionPrivate::stashKeyCookie{u
"_c_csrfcookie"_qs};
47 const QString CSRFProtectionPrivate::stashKeyCookieUsed{u
"_c_csrfcookieused"_qs};
48 const QString CSRFProtectionPrivate::stashKeyCookieNeedsReset{u
"_c_csrfcookieneedsreset"_qs};
49 const QString CSRFProtectionPrivate::stashKeyCookieSet{u
"_c_csrfcookieset"_qs};
50 const QString CSRFProtectionPrivate::stashKeyProcessingDone{u
"_c_csrfprocessingdone"_qs};
51 const QString CSRFProtectionPrivate::stashKeyCheckPassed{u
"_c_csrfcheckpassed"_qs};
55 , d_ptr(new CSRFProtectionPrivate)
59 CSRFProtection::CSRFProtection(
Application *parent,
const QVariantMap &defaultConfig)
61 , d_ptr(new CSRFProtectionPrivate)
64 d->defaultConfig = defaultConfig;
67 CSRFProtection::~CSRFProtection() =
default;
75 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_CSRFProtection_Plugin"_qs);
77 bool cookieExpirationOk =
false;
80 .value(u
"cookie_expiration"_qs,
83 d->defaultConfig.value(
84 u
"cookie_expiration"_qs,
85 static_cast<qint64>(std::chrono::duration_cast<std::chrono::seconds>(
86 CSRFProtectionPrivate::cookieDefaultExpiration)
89 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
91 if (!cookieExpirationOk) {
92 qCWarning(C_CSRFPROTECTION).nospace() <<
"Invalid value set for cookie_expiration. " 93 "Using default value " 94 #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) 95 << CSRFProtectionPrivate::cookieDefaultExpiration;
99 d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
103 config.value(u
"cookie_domain"_qs, d->defaultConfig.value(u
"cookie_domain"_qs)).toString();
104 if (d->cookieName.isEmpty()) {
105 d->cookieName =
"csrftoken";
107 d->cookiePath = u
"/"_qs;
111 .value(u
"cookie_same_site"_qs,
112 d->defaultConfig.value(u
"cookie_same_site"_qs, u
"strict"_qs))
115 d->cookieSameSite = QNetworkCookie::SameSite::Default;
117 d->cookieSameSite = QNetworkCookie::SameSite::None;
119 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
121 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
123 qCWarning(C_CSRFPROTECTION).nospace() <<
"Invalid value set for cookie_same_site. " 124 "Using default value " 125 << QNetworkCookie::SameSite::Strict;
126 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
130 config.value(u
"cookie_secure"_qs, d->defaultConfig.value(u
"cookie_secure"_qs,
false))
133 if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
134 qCWarning(C_CSRFPROTECTION)
135 <<
"cookie_same_site has been set to None but cookie_secure is " 136 "not set to true. Implicitely setting cookie_secure to true. " 137 "Please check your configuration.";
138 d->cookieSecure =
true;
141 if (d->headerName.isEmpty()) {
142 d->headerName =
"X_CSRFTOKEN";
146 config.value(u
"trusted_origins"_qs, d->defaultConfig.value(u
"trusted_origins"_qs))
149 if (d->formInputName.isEmpty()) {
150 d->formInputName =
"csrfprotectiontoken";
153 config.
value(u
"log_failed_ip"_qs, d->defaultConfig.value(u
"log_failed_ip"_qs,
false))
155 if (d->errorMsgStashKey.isEmpty()) {
156 d->errorMsgStashKey = u
"error_msg"_qs;
168 void CSRFProtection::setDefaultDetachTo(
const QString &actionNameOrPath)
171 d->defaultDetachTo = actionNameOrPath;
174 void CSRFProtection::setFormFieldName(
const QByteArray &fieldName)
177 d->formInputName = fieldName;
180 void CSRFProtection::setErrorMsgStashKey(
const QString &keyName)
183 d->errorMsgStashKey = keyName;
186 void CSRFProtection::setIgnoredNamespaces(
const QStringList &namespaces)
189 d->ignoredNamespaces = namespaces;
192 void CSRFProtection::setUseSessions(
bool useSessions)
195 d->useSessions = useSessions;
198 void CSRFProtection::setCookieHttpOnly(
bool httpOnly)
201 d->cookieHttpOnly = httpOnly;
204 void CSRFProtection::setCookieName(
const QByteArray &cookieName)
207 d->cookieName = cookieName;
210 void CSRFProtection::setHeaderName(
const QByteArray &headerName)
213 d->headerName = headerName;
216 void CSRFProtection::setGenericErrorMessage(
const QString &message)
219 d->genericErrorMessage = message;
222 void CSRFProtection::setGenericErrorContentType(
const QByteArray &type)
225 d->genericContentType = type;
232 const QByteArray contextCookie = c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
235 secret = CSRFProtectionPrivate::getNewCsrfString();
236 token = CSRFProtectionPrivate::saltCipherSecret(secret);
237 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, token);
239 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
240 token = CSRFProtectionPrivate::saltCipherSecret(secret);
243 c->
setStash(CSRFProtectionPrivate::stashKeyCookieUsed,
true);
253 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
257 form = QStringLiteral(
"<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
264 bool CSRFProtection::checkPassed(
Context *c)
266 if (CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
269 return c->
stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
285 QByteArray CSRFProtectionPrivate::getNewCsrfString()
289 while (csrfString.
size() < CSRFProtectionPrivate::secretLength) {
294 csrfString.
resize(CSRFProtectionPrivate::secretLength);
307 salted.
reserve(CSRFProtectionPrivate::tokenLength);
309 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
310 std::vector<std::pair<int, int>> pairs;
311 pairs.reserve(std::min(secret.
size(), salt.
size()));
312 for (
int i = 0; i < std::min(secret.
size(), salt.
size()); ++i) {
313 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.
at(i)),
314 CSRFProtectionPrivate::allowedChars.indexOf(salt.
at(i)));
318 cipher.
reserve(CSRFProtectionPrivate::secretLength);
319 for (
const auto &p : std::as_const(pairs)) {
321 CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
322 CSRFProtectionPrivate::allowedChars.size()]);
325 salted = salt + cipher;
339 secret.
reserve(CSRFProtectionPrivate::secretLength);
341 const QByteArray salt = token.
left(CSRFProtectionPrivate::secretLength);
342 const QByteArray _token = token.
mid(CSRFProtectionPrivate::secretLength);
344 std::vector<std::pair<int, int>> pairs;
345 pairs.reserve(std::min(salt.
size(), _token.
size()));
346 for (
int i = 0; i < std::min(salt.
size(), _token.
size()); ++i) {
347 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.
at(i)),
348 CSRFProtectionPrivate::allowedChars.indexOf(salt.
at(i)));
351 for (
const auto &p : std::as_const(pairs)) {
352 QByteArray::size_type idx = p.first - p.second;
354 idx = CSRFProtectionPrivate::allowedChars.size() + idx;
356 secret.
append(CSRFProtectionPrivate::allowedChars.at(idx));
367 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
369 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
382 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe) ||
383 token.
size() != CSRFProtectionPrivate::tokenLength) {
384 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
401 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
405 if (csrf->d_ptr->useSessions) {
413 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
414 if (token != cookieToken) {
415 c->
setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset,
true);
419 qCDebug(C_CSRFPROTECTION) <<
"Got token" << token <<
"from" 420 << (csrf->d_ptr->useSessions ?
"sessions" :
"cookie");
429 void CSRFProtectionPrivate::setToken(
Context *c)
432 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
436 if (csrf->d_ptr->useSessions) {
438 CSRFProtectionPrivate::sessionKey,
439 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
442 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
443 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
444 cookie.
setDomain(csrf->d_ptr->cookieDomain);
446 if (csrf->d_ptr->cookieExpiration.count() == 0) {
449 #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) 450 cookie.setExpirationDate(
453 cookie.setExpirationDate(
457 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
458 cookie.setPath(csrf->d_ptr->cookiePath);
459 cookie.setSecure(csrf->d_ptr->cookieSecure);
460 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
465 qCDebug(C_CSRFPROTECTION) <<
"Set token" 466 << c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
467 <<
"to" << (csrf->d_ptr->useSessions ?
"session" :
"cookie");
475 void CSRFProtectionPrivate::reject(
Context *c,
479 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
false);
482 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
486 if (C_CSRFPROTECTION().isWarningEnabled()) {
487 if (csrf->d_ptr->logFailedIp) {
488 qCWarning(C_CSRFPROTECTION).nospace().noquote()
489 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path() <<
" [" 492 qCWarning(C_CSRFPROTECTION).nospace().noquote()
493 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path()
494 <<
" [IP logging disabled]";
499 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
503 detachToCsrf = csrf->d_ptr->defaultDetachTo;
506 Action *detachToAction =
nullptr;
510 if (!detachToAction) {
513 if (!detachToAction) {
514 qCWarning(C_CSRFPROTECTION)
515 <<
"Can not find action for" << detachToCsrf <<
"to detach to";
519 if (detachToAction) {
520 c->
detach(detachToAction);
523 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
524 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
528 const QString title = c->
qtTrId(
"cutelyst-csrf-generic-error-page-title");
529 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n" 530 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" 534 QStringLiteral(
"</title>\n" 539 QStringLiteral(
"</h1>\n" 542 QStringLiteral(
"</p>\n" 551 void CSRFProtectionPrivate::accept(
Context *c)
553 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
true);
554 c->
setStash(CSRFProtectionPrivate::stashKeyProcessingDone,
true);
563 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
564 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
567 QByteArray::size_type diff = _t1.
size() ^ _t2.
size();
568 for (QByteArray::size_type i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
569 diff |= _t1[i] ^ _t2[i];
578 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
581 CSRFProtectionPrivate::reject(c,
582 u
"CSRFProtection plugin not registered"_qs,
584 c->
qtTrId(
"cutelyst-csrf-reject-not-registered"));
588 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
589 if (!csrfToken.
isNull()) {
590 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
592 CSRFProtection::getToken(c);
595 if (c->
stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
600 qCDebug(C_CSRFPROTECTION).noquote().nospace()
602 <<
" is ignored by the CSRF protection";
606 if (csrf->d_ptr->ignoredNamespaces.contains(c->
action()->
ns())) {
608 qCDebug(C_CSRFPROTECTION)
609 <<
"Namespace" << c->
action()->
ns() <<
"is ignored by the CSRF protection";
616 if (!CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
631 if (c->
req()->secure()) {
634 if (Q_UNLIKELY(referer.isEmpty())) {
635 CSRFProtectionPrivate::reject(c,
636 u
"Referer checking failed - no Referer"_qs,
638 c->
qtTrId(
"cutelyst-csrf-reject-no-referer"));
642 if (Q_UNLIKELY(!refererUrl.isValid())) {
643 CSRFProtectionPrivate::reject(
645 u
"Referer checking failed - Referer is malformed"_qs,
647 c->
qtTrId(
"cutelyst-csrf-reject-referer-malformed"));
650 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"https"))) {
651 CSRFProtectionPrivate::reject(
653 u
"Referer checking failed - Referer is insecure while " 657 c->
qtTrId(
"cutelyst-csrf-reject-refer-insecure"));
663 constexpr
int httpPort = 80;
664 constexpr
int httpsPort = 443;
666 const QUrl uri = c->
req()->uri();
668 if (!csrf->d_ptr->useSessions) {
669 goodReferer = csrf->d_ptr->cookieDomain;
672 goodReferer = uri.
host();
674 const int serverPort = uri.
port(c->
req()->secure() ? httpsPort : httpPort);
675 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
679 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
680 goodHosts.
append(goodReferer);
682 QString refererHost = refererUrl.host();
683 const int refererPort = refererUrl.port(
684 refererUrl.scheme().compare(u
"https") == 0 ? httpsPort : httpPort);
685 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
689 bool refererCheck =
false;
690 for (
const auto &host : std::as_const(goodHosts)) {
691 if ((host.startsWith(u
'.') &&
692 (refererHost.
endsWith(host) || (refererHost == host.
mid(1)))) ||
693 host == refererHost) {
699 if (Q_UNLIKELY(!refererCheck)) {
701 CSRFProtectionPrivate::reject(
703 u
"Referer checking failed - %1 does not match any " 707 c->
qtTrId(
"cutelyst-csrf-reject-referer-no-trust")
716 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
717 CSRFProtectionPrivate::reject(c,
718 u
"CSRF cookie not set"_qs,
720 c->
qtTrId(
"cutelyst-csrf-reject-no-cookie"));
727 if (c->
req()->contentType().
compare(
"multipart/form-data") == 0) {
731 if (upload && upload->
size() < 1024 ) {
732 requestCsrfToken = upload->
readAll();
741 if (requestCsrfToken.
isEmpty()) {
742 requestCsrfToken = c->
req()->
header(csrf->d_ptr->headerName);
743 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
744 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
745 <<
"from HTTP header" << csrf->d_ptr->headerName;
747 qCDebug(C_CSRFPROTECTION)
748 <<
"Can not get token from HTTP header or form field.";
751 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
752 <<
"from form field" << csrf->d_ptr->formInputName;
755 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
758 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
759 CSRFProtectionPrivate::reject(c,
760 u
"CSRF token missing or incorrect"_qs,
762 c->
qtTrId(
"cutelyst-csrf-reject-token-missin"));
769 CSRFProtectionPrivate::accept(c);
776 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
777 if (c->
stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
782 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
786 CSRFProtectionPrivate::setToken(c);
787 c->
setStash(CSRFProtectionPrivate::stashKeyCookieSet,
true);
790 #include "moc_csrfprotection.cpp" ParamsMultiMap attributes() const noexcept
QByteArray toByteArray() const const
void setCookie(const QNetworkCookie &cookie)
QByteArray header(QByteArrayView key) const noexcept
void postForked(Cutelyst::Application *app)
Upload * upload(QStringView name) const
void reserve(qsizetype size)
char at(qsizetype i) const const
Headers & headers() noexcept
QStringList split(const QString &sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool isDelete() const noexcept
Response * res() const noexcept
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)
Action * getActionByPath(QStringView path) const
int port(int defaultPort) const const
QString reverse() const noexcept
void setContentType(const QByteArray &type)
This class represents a Cutelyst Action.
T value(qsizetype i) const const
int compare(QByteArrayView bv, Qt::CaseSensitivity cs) const const
Cutelyst Upload handles file upload requests.
void resize(qsizetype size)
bool setup(Application *app) override
QString number(int n, int base)
void append(QList::parameter_type value)
void setDomain(const QString &domain)
QString addressString() const
void stash(const QVariantHash &unite)
Headers headers() const noexcept
QVariantMap config(const QString &entity) const
bool isEmpty() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
static void setValue(Context *c, const QString &key, const QVariant &value)
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
The Cutelyst namespace holds all public Cutelyst API.
QByteArray mid(qsizetype pos, qsizetype len) const const
QByteArray & append(char ch)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString ns() const noexcept
void beforeDispatch(Cutelyst::Context *c)
bool contains(const Key &key) const const
QString fromLatin1(QByteArrayView str)
QByteArray left(qsizetype len) const const
QDateTime currentDateTime()
QByteArray toLatin1() const const
QString mid(qsizetype position, qsizetype n) const const
QString qtTrId(const char *id, int n=-1) const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
CUTELYST_LIBRARY std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Base class for Cutelyst Plugins.
The Cutelyst application.
QByteArray cookie(QByteArrayView name) const
Engine * engine() const noexcept
QString bodyParam(const QString &key, const QString &defaultValue={}) const
Action * actionFor(QStringView name) const
void setBody(QIODevice *body)
QString attribute(const QString &name, const QString &defaultValue={}) const
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
int compare(const QString &other, Qt::CaseSensitivity cs) const const
void setStatus(quint16 status) noexcept
QString className() const noexcept
qint64 size() const override
Dispatcher * dispatcher() const noexcept
void loadTranslations(const QString &filename, const QString &directory={}, const QString &prefix={}, const QString &suffix={})