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 QByteArray CSRFProtection::formFieldName() noexcept
183 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
187 return csrf->d_ptr->formInputName;
190 void CSRFProtection::setErrorMsgStashKey(
const QString &keyName)
193 d->errorMsgStashKey = keyName;
196 void CSRFProtection::setIgnoredNamespaces(
const QStringList &namespaces)
199 d->ignoredNamespaces = namespaces;
202 void CSRFProtection::setUseSessions(
bool useSessions)
205 d->useSessions = useSessions;
208 void CSRFProtection::setCookieHttpOnly(
bool httpOnly)
211 d->cookieHttpOnly = httpOnly;
214 void CSRFProtection::setCookieName(
const QByteArray &cookieName)
217 d->cookieName = cookieName;
220 void CSRFProtection::setHeaderName(
const QByteArray &headerName)
223 d->headerName = headerName;
226 void CSRFProtection::setGenericErrorMessage(
const QString &message)
229 d->genericErrorMessage = message;
232 void CSRFProtection::setGenericErrorContentType(
const QByteArray &type)
235 d->genericContentType = type;
242 const QByteArray contextCookie = c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
245 secret = CSRFProtectionPrivate::getNewCsrfString();
246 token = CSRFProtectionPrivate::saltCipherSecret(secret);
247 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, token);
249 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
250 token = CSRFProtectionPrivate::saltCipherSecret(secret);
253 c->
setStash(CSRFProtectionPrivate::stashKeyCookieUsed,
true);
263 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
267 form = QStringLiteral(
"<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
274 bool CSRFProtection::checkPassed(
Context *c)
276 if (CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
279 return c->
stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
295 QByteArray CSRFProtectionPrivate::getNewCsrfString()
299 while (csrfString.
size() < CSRFProtectionPrivate::secretLength) {
304 csrfString.
resize(CSRFProtectionPrivate::secretLength);
317 salted.
reserve(CSRFProtectionPrivate::tokenLength);
319 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
320 std::vector<std::pair<int, int>> pairs;
321 pairs.reserve(std::min(secret.
size(), salt.
size()));
322 for (
int i = 0; i < std::min(secret.
size(), salt.
size()); ++i) {
323 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.
at(i)),
324 CSRFProtectionPrivate::allowedChars.indexOf(salt.
at(i)));
328 cipher.
reserve(CSRFProtectionPrivate::secretLength);
329 for (
const auto &p : std::as_const(pairs)) {
331 CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
332 CSRFProtectionPrivate::allowedChars.size()]);
335 salted = salt + cipher;
349 secret.
reserve(CSRFProtectionPrivate::secretLength);
351 const QByteArray salt = token.
left(CSRFProtectionPrivate::secretLength);
352 const QByteArray _token = token.
mid(CSRFProtectionPrivate::secretLength);
354 std::vector<std::pair<int, int>> pairs;
356 for (
int i = 0; i < std::min(salt.
size(), _token.
size()); ++i) {
357 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.
at(i)),
358 CSRFProtectionPrivate::allowedChars.indexOf(salt.
at(i)));
361 for (
const auto &p : std::as_const(pairs)) {
362 QByteArray::size_type idx = p.first - p.second;
364 idx = CSRFProtectionPrivate::allowedChars.size() + idx;
366 secret.
append(CSRFProtectionPrivate::allowedChars.at(idx));
377 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
379 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
392 if (tokenString.
contains(CSRFProtectionPrivate::sanitizeRe) ||
393 token.
size() != CSRFProtectionPrivate::tokenLength) {
394 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
411 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
415 if (csrf->d_ptr->useSessions) {
423 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
424 if (token != cookieToken) {
425 c->
setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset,
true);
429 qCDebug(C_CSRFPROTECTION) <<
"Got token" << token <<
"from"
430 << (csrf->d_ptr->useSessions ?
"sessions" :
"cookie");
439 void CSRFProtectionPrivate::setToken(
Context *c)
442 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
446 if (csrf->d_ptr->useSessions) {
448 CSRFProtectionPrivate::sessionKey,
449 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
452 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
453 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
454 cookie.setDomain(csrf->d_ptr->cookieDomain);
456 if (csrf->d_ptr->cookieExpiration.count() == 0) {
459 #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
460 cookie.setExpirationDate(
463 cookie.setExpirationDate(
467 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
468 cookie.setPath(csrf->d_ptr->cookiePath);
469 cookie.setSecure(csrf->d_ptr->cookieSecure);
470 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
475 qCDebug(C_CSRFPROTECTION) <<
"Set token"
476 << c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
477 <<
"to" << (csrf->d_ptr->useSessions ?
"session" :
"cookie");
485 void CSRFProtectionPrivate::reject(
Context *c,
489 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
false);
492 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
496 if (C_CSRFPROTECTION().isWarningEnabled()) {
497 if (csrf->d_ptr->logFailedIp) {
498 qCWarning(C_CSRFPROTECTION).nospace().noquote()
499 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path() <<
" ["
502 qCWarning(C_CSRFPROTECTION).nospace().noquote()
503 <<
"Forbidden: (" << logReason <<
"): " << c->
req()->path()
504 <<
" [IP logging disabled]";
509 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
513 detachToCsrf = csrf->d_ptr->defaultDetachTo;
516 Action *detachToAction =
nullptr;
520 if (!detachToAction) {
523 if (!detachToAction) {
524 qCWarning(C_CSRFPROTECTION)
525 <<
"Can not find action for" << detachToCsrf <<
"to detach to";
529 if (detachToAction) {
530 c->
detach(detachToAction);
533 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
534 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
538 const QString title = c->
qtTrId(
"cutelyst-csrf-generic-error-page-title");
539 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
540 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
544 QStringLiteral(
"</title>\n"
549 QStringLiteral(
"</h1>\n"
552 QStringLiteral(
"</p>\n"
561 void CSRFProtectionPrivate::accept(
Context *c)
563 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
true);
564 c->
setStash(CSRFProtectionPrivate::stashKeyProcessingDone,
true);
573 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
574 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
577 QByteArray::size_type diff = _t1.
size() ^ _t2.
size();
578 for (QByteArray::size_type i = 0; i < _t1.
size() && i < _t2.
size(); i++) {
579 diff |= _t1[i] ^ _t2[i];
588 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
591 CSRFProtectionPrivate::reject(c,
592 u
"CSRFProtection plugin not registered"_qs,
594 c->
qtTrId(
"cutelyst-csrf-reject-not-registered"));
598 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
599 if (!csrfToken.
isNull()) {
600 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
602 CSRFProtection::getToken(c);
605 if (c->
stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
610 qCDebug(C_CSRFPROTECTION).noquote().nospace()
612 <<
" is ignored by the CSRF protection";
616 if (csrf->d_ptr->ignoredNamespaces.contains(c->
action()->
ns())) {
618 qCDebug(C_CSRFPROTECTION)
619 <<
"Namespace" << c->
action()->
ns() <<
"is ignored by the CSRF protection";
626 if (!CSRFProtectionPrivate::secureMethods.contains(c->
req()->method())) {
641 if (c->
req()->secure()) {
644 if (Q_UNLIKELY(referer.isEmpty())) {
645 CSRFProtectionPrivate::reject(c,
646 u
"Referer checking failed - no Referer"_qs,
648 c->
qtTrId(
"cutelyst-csrf-reject-no-referer"));
652 if (Q_UNLIKELY(!refererUrl.isValid())) {
653 CSRFProtectionPrivate::reject(
655 u
"Referer checking failed - Referer is malformed"_qs,
657 c->
qtTrId(
"cutelyst-csrf-reject-referer-malformed"));
660 if (Q_UNLIKELY(refererUrl.scheme() !=
QLatin1String(
"https"))) {
661 CSRFProtectionPrivate::reject(
663 u
"Referer checking failed - Referer is insecure while "
667 c->
qtTrId(
"cutelyst-csrf-reject-refer-insecure"));
673 constexpr
int httpPort = 80;
674 constexpr
int httpsPort = 443;
676 const QUrl uri = c->
req()->uri();
678 if (!csrf->d_ptr->useSessions) {
679 goodReferer = csrf->d_ptr->cookieDomain;
682 goodReferer = uri.
host();
684 const int serverPort = uri.
port(c->
req()->secure() ? httpsPort : httpPort);
685 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
689 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
690 goodHosts.
append(goodReferer);
692 QString refererHost = refererUrl.host();
693 const int refererPort = refererUrl.port(
694 refererUrl.scheme().compare(u
"https") == 0 ? httpsPort : httpPort);
695 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
699 bool refererCheck =
false;
700 for (
const auto &host : std::as_const(goodHosts)) {
701 if ((host.startsWith(u
'.') &&
702 (refererHost.
endsWith(host) || (refererHost == host.mid(1)))) ||
703 host == refererHost) {
709 if (Q_UNLIKELY(!refererCheck)) {
711 CSRFProtectionPrivate::reject(
713 u
"Referer checking failed - %1 does not match any "
717 c->
qtTrId(
"cutelyst-csrf-reject-referer-no-trust")
726 if (Q_UNLIKELY(csrfToken.
isEmpty())) {
727 CSRFProtectionPrivate::reject(c,
728 u
"CSRF cookie not set"_qs,
730 c->
qtTrId(
"cutelyst-csrf-reject-no-cookie"));
737 if (c->
req()->contentType().
compare(
"multipart/form-data") == 0) {
741 if (upload && upload->
size() < 1024 ) {
742 requestCsrfToken = upload->
readAll();
751 if (requestCsrfToken.
isEmpty()) {
752 requestCsrfToken = c->
req()->
header(csrf->d_ptr->headerName);
753 if (Q_LIKELY(!requestCsrfToken.
isEmpty())) {
754 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
755 <<
"from HTTP header" << csrf->d_ptr->headerName;
757 qCDebug(C_CSRFPROTECTION)
758 <<
"Can not get token from HTTP header or form field.";
761 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
762 <<
"from form field" << csrf->d_ptr->formInputName;
765 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
768 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
769 CSRFProtectionPrivate::reject(c,
770 u
"CSRF token missing or incorrect"_qs,
772 c->
qtTrId(
"cutelyst-csrf-reject-token-missin"));
779 CSRFProtectionPrivate::accept(c);
786 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
787 if (c->
stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
792 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
796 CSRFProtectionPrivate::setToken(c);
797 c->
setStash(CSRFProtectionPrivate::stashKeyCookieSet,
true);
800 #include "moc_csrfprotection.cpp"
This class represents a Cutelyst Action.
QString ns() const noexcept
QString className() const noexcept
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)
void loadTranslations(const QString &filename, const QString &directory={}, const QString &prefix={}, const QString &suffix={})
void postForked(Cutelyst::Application *app)
Protect input forms against Cross Site Request Forgery (CSRF/XSRF) attacks.
QString reverse() const noexcept
void stash(const QVariantHash &unite)
void detach(Action *action=nullptr)
Response * res() const noexcept
void setStash(const QString &key, const QVariant &value)
QString qtTrId(const char *id, int n=-1) const
Dispatcher * dispatcher() const noexcept
Action * actionFor(QStringView name) const
Action * getActionByPath(QStringView path) const
QVariantMap config(const QString &entity) const
Base class for Cutelyst Plugins.
QString addressString() const
bool isDelete() const noexcept
QByteArray cookie(QByteArrayView name) const
Upload * upload(QStringView name) const
Headers headers() const noexcept
QString bodyParam(const QString &key, const QString &defaultValue={}) const
QByteArray header(QByteArrayView key) const noexcept
void setContentType(const QByteArray &type)
void setStatus(quint16 status) noexcept
Headers & headers() noexcept
void setBody(QIODevice *body)
void setCookie(const QNetworkCookie &cookie)
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 requests.
qint64 size() const override
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
The Cutelyst namespace holds all public Cutelyst API.
QByteArray & append(QByteArrayView data)
char at(qsizetype i) const const
int compare(QByteArrayView bv, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool isNull() const const
QByteArray left(qsizetype len) const const
QByteArray mid(qsizetype pos, qsizetype len) const const
void reserve(qsizetype size)
void resize(qsizetype newSize, char c)
qsizetype size() const const
QDateTime currentDateTime()
void append(QList::parameter_type value)
T value(qsizetype i) const const
bool contains(const Key &key) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString arg(Args &&... args) const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QStringList split(QChar sep, Qt::SplitBehavior behavior, 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