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>
29Q_LOGGING_CATEGORY(C_CSRFPROTECTION,
"cutelyst.plugin.csrfprotection", QtWarningMsg)
35const QRegularExpression CSRFProtectionPrivate::sanitizeRe{u
"[^a-zA-Z0-9\\-_]"_qs};
37const QByteArrayList CSRFProtectionPrivate::secureMethods = QByteArrayList({
43const QByteArray CSRFProtectionPrivate::allowedChars{
44 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"_qba};
45const QString CSRFProtectionPrivate::sessionKey{u
"_csrftoken"_qs};
46const QString CSRFProtectionPrivate::stashKeyCookie{u
"_c_csrfcookie"_qs};
47const QString CSRFProtectionPrivate::stashKeyCookieUsed{u
"_c_csrfcookieused"_qs};
48const QString CSRFProtectionPrivate::stashKeyCookieNeedsReset{u
"_c_csrfcookieneedsreset"_qs};
49const QString CSRFProtectionPrivate::stashKeyCookieSet{u
"_c_csrfcookieset"_qs};
50const QString CSRFProtectionPrivate::stashKeyProcessingDone{u
"_c_csrfprocessingdone"_qs};
51const QString CSRFProtectionPrivate::stashKeyCheckPassed{u
"_c_csrfcheckpassed"_qs};
55 , d_ptr(new CSRFProtectionPrivate)
67 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_CSRFProtection_Plugin"_qs);
69 bool cookieExpirationOk =
false;
70 const QString cookieExpireStr =
73 u
"cookie_expiration"_qs,
74 config.value(u
"cookie_age"_qs,
75 static_cast<qint64
>(std::chrono::duration_cast<std::chrono::seconds>(
76 CSRFProtectionPrivate::cookieDefaultExpiration)
79 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
81 if (!cookieExpirationOk) {
82 qCWarning(C_CSRFPROTECTION).nospace() <<
"Invalid value set for cookie_expiration. "
83 "Using default value "
84#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
85 << CSRFProtectionPrivate::cookieDefaultExpiration;
89 d->cookieExpiration = CSRFProtectionPrivate::cookieDefaultExpiration;
92 d->cookieDomain = config.value(u
"cookie_domain"_qs).toString();
93 if (d->cookieName.isEmpty()) {
94 d->cookieName =
"csrftoken";
96 d->cookiePath = u
"/"_qs;
98 const QString _sameSite = config.value(u
"cookie_same_site"_qs, u
"strict"_qs).toString();
99 if (_sameSite.compare(u
"default", Qt::CaseInsensitive) == 0) {
100 d->cookieSameSite = QNetworkCookie::SameSite::Default;
101 }
else if (_sameSite.compare(u
"none", Qt::CaseInsensitive) == 0) {
102 d->cookieSameSite = QNetworkCookie::SameSite::None;
103 }
else if (_sameSite.compare(u
"lax", Qt::CaseInsensitive) == 0) {
104 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
105 }
else if (_sameSite.compare(u
"strict", Qt::CaseInsensitive) == 0) {
106 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
108 qCWarning(C_CSRFPROTECTION).nospace() <<
"Invalid value set for cookie_same_site. "
109 "Using default value "
110 << QNetworkCookie::SameSite::Strict;
111 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
114 d->cookieSecure = config.value(u
"cookie_secure"_qs,
false).toBool();
116 if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
117 qCWarning(C_CSRFPROTECTION)
118 <<
"cookie_same_site has been set to None but cookie_secure is "
119 "not set to true. Implicitely setting cookie_secure to true. "
120 "Please check your configuration.";
121 d->cookieSecure =
true;
124 if (d->headerName.isEmpty()) {
125 d->headerName =
"X_CSRFTOKEN";
129 config.value(u
"trusted_origins"_qs).toString().split(u
',', Qt::SkipEmptyParts);
130 if (d->formInputName.isEmpty()) {
131 d->formInputName =
"csrfprotectiontoken";
133 d->logFailedIp = config.value(u
"log_failed_ip"_qs,
false).toBool();
134 if (d->errorMsgStashKey.isEmpty()) {
135 d->errorMsgStashKey = u
"error_msg"_qs;
150 d->defaultDetachTo = actionNameOrPath;
156 d->formInputName = fieldName;
162 d->errorMsgStashKey = keyName;
168 d->ignoredNamespaces = namespaces;
174 d->useSessions = useSessions;
180 d->cookieHttpOnly = httpOnly;
186 d->cookieName = cookieName;
192 d->headerName = headerName;
198 d->genericErrorMessage = message;
204 d->genericContentType = type;
211 const QByteArray contextCookie = c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray();
213 if (contextCookie.isEmpty()) {
214 secret = CSRFProtectionPrivate::getNewCsrfString();
215 token = CSRFProtectionPrivate::saltCipherSecret(secret);
216 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, token);
218 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
219 token = CSRFProtectionPrivate::saltCipherSecret(secret);
222 c->
setStash(CSRFProtectionPrivate::stashKeyCookieUsed,
true);
232 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
236 form = QStringLiteral(
"<input type=\"hidden\" name=\"%1\" value=\"%2\" />")
237 .arg(QString::fromLatin1(csrf->d_ptr->formInputName),
245 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
248 return c->
stash(CSRFProtectionPrivate::stashKeyCheckPassed).toBool();
264QByteArray CSRFProtectionPrivate::getNewCsrfString()
266 QByteArray csrfString;
268 while (csrfString.size() < CSRFProtectionPrivate::secretLength) {
269 csrfString.append(QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding |
270 QByteArray::OmitTrailingEquals));
273 csrfString.resize(CSRFProtectionPrivate::secretLength);
283QByteArray CSRFProtectionPrivate::saltCipherSecret(
const QByteArray &secret)
286 salted.reserve(CSRFProtectionPrivate::tokenLength);
288 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
289 std::vector<std::pair<int, int>> pairs;
290 pairs.reserve(std::min(secret.size(), salt.size()));
291 for (
int i = 0; i < std::min(secret.size(), salt.size()); ++i) {
292 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(secret.at(i)),
293 CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
297 cipher.reserve(CSRFProtectionPrivate::secretLength);
298 for (
const auto &p : std::as_const(pairs)) {
300 CSRFProtectionPrivate::allowedChars[(p.first + p.second) %
301 CSRFProtectionPrivate::allowedChars.size()]);
304 salted = salt + cipher;
315QByteArray CSRFProtectionPrivate::unsaltCipherToken(
const QByteArray &token)
318 secret.reserve(CSRFProtectionPrivate::secretLength);
320 const QByteArray salt = token.left(CSRFProtectionPrivate::secretLength);
321 const QByteArray _token = token.mid(CSRFProtectionPrivate::secretLength);
323 std::vector<std::pair<int, int>> pairs;
324 pairs.reserve(std::min(salt.size(), _token.size()));
325 for (
int i = 0; i < std::min(salt.size(), _token.size()); ++i) {
326 pairs.emplace_back(CSRFProtectionPrivate::allowedChars.indexOf(_token.at(i)),
327 CSRFProtectionPrivate::allowedChars.indexOf(salt.at(i)));
330 for (
const auto &p : std::as_const(pairs)) {
331 QByteArray::size_type idx = p.first - p.second;
333 idx = CSRFProtectionPrivate::allowedChars.size() + idx;
335 secret.append(CSRFProtectionPrivate::allowedChars.at(idx));
346QByteArray CSRFProtectionPrivate::getNewCsrfToken()
348 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
356QByteArray CSRFProtectionPrivate::sanitizeToken(
const QByteArray &token)
358 QByteArray sanitized;
360 const QString tokenString = QString::fromLatin1(token);
361 if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe) ||
362 token.size() != CSRFProtectionPrivate::tokenLength) {
363 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
375QByteArray CSRFProtectionPrivate::getToken(
Context *c)
380 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
384 if (csrf->d_ptr->useSessions) {
385 token =
Session::value(c, CSRFProtectionPrivate::sessionKey).toByteArray();
387 QByteArray cookieToken = c->req()->
cookie(csrf->d_ptr->cookieName);
388 if (cookieToken.isEmpty()) {
392 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
393 if (token != cookieToken) {
394 c->
setStash(CSRFProtectionPrivate::stashKeyCookieNeedsReset,
true);
398 qCDebug(C_CSRFPROTECTION) <<
"Got token" << token <<
"from"
399 << (csrf->d_ptr->useSessions ?
"sessions" :
"cookie");
408void CSRFProtectionPrivate::setToken(
Context *c)
411 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
415 if (csrf->d_ptr->useSessions) {
417 CSRFProtectionPrivate::sessionKey,
418 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
420 QNetworkCookie cookie(csrf->d_ptr->cookieName,
421 c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray());
422 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
423 cookie.setDomain(csrf->d_ptr->cookieDomain);
425 if (csrf->d_ptr->cookieExpiration.count() == 0) {
426 cookie.setExpirationDate(QDateTime());
428#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
429 cookie.setExpirationDate(
430 QDateTime::currentDateTime().addDuration(csrf->d_ptr->cookieExpiration));
432 cookie.setExpirationDate(
433 QDateTime::currentDateTime().addSecs(csrf->d_ptr->cookieExpiration.count()));
436 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
437 cookie.setPath(csrf->d_ptr->cookiePath);
438 cookie.setSecure(csrf->d_ptr->cookieSecure);
439 cookie.setSameSitePolicy(csrf->d_ptr->cookieSameSite);
444 qCDebug(C_CSRFPROTECTION) <<
"Set token"
445 << c->
stash(CSRFProtectionPrivate::stashKeyCookie).toByteArray()
446 <<
"to" << (csrf->d_ptr->useSessions ?
"session" :
"cookie");
454void CSRFProtectionPrivate::reject(
Context *c,
455 const QString &logReason,
456 const QString &displayReason)
458 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
false);
461 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
465 if (C_CSRFPROTECTION().isWarningEnabled()) {
466 if (csrf->d_ptr->logFailedIp) {
467 qCWarning(C_CSRFPROTECTION).nospace().noquote()
468 <<
"Forbidden: (" << logReason <<
"): " << c->req()->path() <<
" ["
471 qCWarning(C_CSRFPROTECTION).nospace().noquote()
472 <<
"Forbidden: (" << logReason <<
"): " << c->req()->path()
473 <<
" [IP logging disabled]";
478 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
480 QString detachToCsrf = c->action()->
attribute(u
"CSRFDetachTo"_qs);
481 if (detachToCsrf.isEmpty()) {
482 detachToCsrf = csrf->d_ptr->defaultDetachTo;
485 Action *detachToAction =
nullptr;
487 if (!detachToCsrf.isEmpty()) {
488 detachToAction = c->controller()->
actionFor(detachToCsrf);
489 if (!detachToAction) {
492 if (!detachToAction) {
493 qCWarning(C_CSRFPROTECTION)
494 <<
"Can not find action for" << detachToCsrf <<
"to detach to";
498 if (detachToAction) {
499 c->
detach(detachToAction);
502 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
503 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
506 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
507 "403 Forbidden - CSRF protection check failed");
508 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
509 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
513 QStringLiteral(
"</title>\n"
518 QStringLiteral(
"</h1>\n"
521 QStringLiteral(
"</p>\n"
530void CSRFProtectionPrivate::accept(
Context *c)
532 c->
setStash(CSRFProtectionPrivate::stashKeyCheckPassed,
true);
533 c->
setStash(CSRFProtectionPrivate::stashKeyProcessingDone,
true);
540bool CSRFProtectionPrivate::compareSaltedTokens(
const QByteArray &t1,
const QByteArray &t2)
542 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
543 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
546 QByteArray::size_type diff = _t1.size() ^ _t2.size();
547 for (QByteArray::size_type i = 0; i < _t1.size() && i < _t2.size(); i++) {
548 diff |= _t1[i] ^ _t2[i];
557void CSRFProtectionPrivate::beforeDispatch(
Context *c)
560 CSRFProtectionPrivate::reject(
562 u
"CSRFProtection plugin not registered"_qs,
564 "The CSRF protection plugin has not been registered."));
568 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
569 if (!csrfToken.isNull()) {
570 c->
setStash(CSRFProtectionPrivate::stashKeyCookie, csrfToken);
575 if (c->
stash(CSRFProtectionPrivate::stashKeyProcessingDone).toBool()) {
579 if (c->action()->
attributes().contains(u
"CSRFIgnore"_qs)) {
580 qCDebug(C_CSRFPROTECTION).noquote().nospace()
582 <<
" is ignored by the CSRF protection";
586 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
587 if (!c->action()->
attributes().contains(u
"CSRFRequire"_qs)) {
588 qCDebug(C_CSRFPROTECTION)
589 <<
"Namespace" << c->action()->
ns() <<
"is ignored by the CSRF protection";
596 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
611 if (c->req()->secure()) {
614 if (Q_UNLIKELY(referer.isEmpty())) {
615 CSRFProtectionPrivate::reject(
617 u
"Referer checking failed - no Referer"_qs,
619 "Referer checking failed - no Referer."));
622 const QUrl refererUrl(QString::fromLatin1(referer));
623 if (Q_UNLIKELY(!refererUrl.isValid())) {
624 CSRFProtectionPrivate::reject(
626 u
"Referer checking failed - Referer is malformed"_qs,
628 "Referer checking failed - Referer is malformed."));
631 if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String(
"https"))) {
632 CSRFProtectionPrivate::reject(
634 u
"Referer checking failed - Referer is insecure while "
637 "Referer checking failed - Referer is insecure while host "
644 constexpr int httpPort = 80;
645 constexpr int httpsPort = 443;
647 const QUrl uri = c->req()->uri();
649 if (!csrf->d_ptr->useSessions) {
650 goodReferer = csrf->d_ptr->cookieDomain;
652 if (goodReferer.isEmpty()) {
653 goodReferer = uri.host();
655 const int serverPort = uri.port(c->req()->secure() ? httpsPort : httpPort);
656 if ((serverPort != httpPort) && (serverPort != httpsPort)) {
657 goodReferer += u
':' + QString::number(serverPort);
660 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
661 goodHosts.append(goodReferer);
663 QString refererHost = refererUrl.host();
664 const int refererPort = refererUrl.port(
665 refererUrl.scheme().compare(u
"https") == 0 ? httpsPort : httpPort);
666 if ((refererPort != httpPort) && (refererPort != httpsPort)) {
667 refererHost += u
':' + QString::number(refererPort);
670 bool refererCheck =
false;
671 for (
const auto &host : std::as_const(goodHosts)) {
672 if ((host.startsWith(u
'.') &&
673 (refererHost.endsWith(host) || (refererHost == host.mid(1)))) ||
674 host == refererHost) {
680 if (Q_UNLIKELY(!refererCheck)) {
682 CSRFProtectionPrivate::reject(
684 u
"Referer checking failed - %1 does not match any "
685 "trusted origins"_qs.arg(QString::fromLatin1(referer)),
687 "Referer checking failed - %1 does not match any "
689 .arg(QString::fromLatin1(referer)));
697 if (Q_UNLIKELY(csrfToken.isEmpty())) {
698 CSRFProtectionPrivate::reject(
700 u
"CSRF cookie not set"_qs,
701 c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
705 QByteArray requestCsrfToken;
708 if (c->req()->contentType().compare(
"multipart/form-data") == 0) {
711 c->req()->
upload(QString::fromLatin1(csrf->d_ptr->formInputName));
712 if (upload && upload->
size() < 1024 ) {
713 requestCsrfToken = upload->readAll();
718 ->
bodyParam(QString::fromLatin1(csrf->d_ptr->formInputName))
722 if (requestCsrfToken.isEmpty()) {
723 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName);
724 if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
725 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
726 <<
"from HTTP header" << csrf->d_ptr->headerName;
728 qCDebug(C_CSRFPROTECTION)
729 <<
"Can not get token from HTTP header or form field.";
732 qCDebug(C_CSRFPROTECTION) <<
"Got token" << requestCsrfToken
733 <<
"from form field" << csrf->d_ptr->formInputName;
736 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
739 !CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
740 CSRFProtectionPrivate::reject(c,
741 u
"CSRF token missing or incorrect"_qs,
743 "CSRF token missing or incorrect."));
750 CSRFProtectionPrivate::accept(c);
757 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieNeedsReset).toBool()) {
758 if (c->
stash(CSRFProtectionPrivate::stashKeyCookieSet).toBool()) {
763 if (!c->
stash(CSRFProtectionPrivate::stashKeyCookieUsed).toBool()) {
767 CSRFProtectionPrivate::setToken(c);
768 c->
setStash(CSRFProtectionPrivate::stashKeyCookieSet,
true);
771#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)
T plugin()
Returns the registered plugin that casts to the template type T.
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.
static bool checkPassed(Context *c)
void setUseSessions(bool useSessions)
void setIgnoredNamespaces(const QStringList &namespaces)
void setDefaultDetachTo(const QString &actionNameOrPath)
void setGenericErrorContentType(const QByteArray &type)
void setHeaderName(const QByteArray &headerName)
void setErrorMsgStashKey(const QString &keyName)
void setFormFieldName(const QByteArray &fieldName)
void setCookieHttpOnly(bool httpOnly)
static QByteArray getToken(Context *c)
~CSRFProtection() override
void setGenericErrorMessage(const QString &message)
bool setup(Application *app) override
void setCookieName(const QByteArray &cookieName)
static QString getTokenFormField(Context *c)
CSRFProtection(Application *parent)
QString reverse() const noexcept
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(QStringView name) const
Action * getActionByPath(QStringView path) const
QVariantMap config(const QString &entity) const
user configuration for the application
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
void setBody(QIODevice *body)
Headers & headers() noexcept
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 request
qint64 size() const override
CUTELYST_LIBRARY std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
The Cutelyst namespace holds all public Cutelyst API.