19 #include "csrfprotection_p.h"
21 #include <Cutelyst/Application>
22 #include <Cutelyst/Engine>
23 #include <Cutelyst/Context>
24 #include <Cutelyst/Request>
25 #include <Cutelyst/Response>
26 #include <Cutelyst/Plugins/Session/Session>
27 #include <Cutelyst/Headers>
28 #include <Cutelyst/Action>
29 #include <Cutelyst/Dispatcher>
30 #include <Cutelyst/Controller>
31 #include <Cutelyst/Upload>
33 #include <QLoggingCategory>
34 #include <QNetworkCookie>
41 #define DEFAULT_COOKIE_AGE Q_INT64_C(31449600) // approx. 1 year
42 #define DEFAULT_COOKIE_NAME "csrftoken"
43 #define DEFAULT_COOKIE_PATH "/"
44 #define DEFAULT_HEADER_NAME "X_CSRFTOKEN"
45 #define DEFAULT_FORM_INPUT_NAME "csrfprotectiontoken"
46 #define CSRF_SECRET_LENGTH 32
47 #define CSRF_TOKEN_LENGTH 2 * CSRF_SECRET_LENGTH
48 #define CSRF_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
49 #define CSRF_SESSION_KEY "_csrftoken"
50 #define CONTEXT_CSRF_COOKIE QStringLiteral("_c_csrfcookie")
51 #define CONTEXT_CSRF_COOKIE_USED QStringLiteral("_c_csrfcookieused")
52 #define CONTEXT_CSRF_COOKIE_NEEDS_RESET QStringLiteral("_c_csrfcookieneedsreset")
53 #define CONTEXT_CSRF_PROCESSING_DONE QStringLiteral("_c_csrfprocessingdone")
54 #define CONTEXT_CSRF_COOKIE_SET QStringLiteral("_c_csrfcookieset")
55 #define CONTEXT_CSRF_CHECK_PASSED QStringLiteral("_c_csrfcheckpassed")
57 Q_LOGGING_CATEGORY(C_CSRFPROTECTION,
"cutelyst.plugin.csrfprotection", QtWarningMsg)
62 const QRegularExpression CSRFProtectionPrivate::sanitizeRe = QRegularExpression(QStringLiteral(
"[^a-zA-Z0-9\\-_]"));
64 const QStringList CSRFProtectionPrivate::secureMethods = QStringList({QStringLiteral(
"GET"), QStringLiteral(
"HEAD"), QStringLiteral(
"OPTIONS"), QStringLiteral(
"TRACE")});
67 , d_ptr(new CSRFProtectionPrivate)
83 const QVariantMap config = app->
engine()->
config(QStringLiteral(
"Cutelyst_CSRFProtection_Plugin"));
85 d->cookieAge = config.value(QStringLiteral(
"cookie_age"), DEFAULT_COOKIE_AGE).value<qint64>();
86 if (d->cookieAge <= 0) {
87 d->cookieAge = DEFAULT_COOKIE_AGE;
89 d->cookieDomain = config.value(QStringLiteral(
"cookie_domain")).toString();
90 if (d->cookieName.isEmpty()) {
91 d->cookieName = QStringLiteral(DEFAULT_COOKIE_NAME);
93 d->cookiePath = QStringLiteral(DEFAULT_COOKIE_PATH);
94 d->cookieSecure = config.value(QStringLiteral(
"cookie_secure"),
false).toBool();
95 if (d->headerName.isEmpty()) {
96 d->headerName = QStringLiteral(DEFAULT_HEADER_NAME);
98 d->trustedOrigins = config.value(QStringLiteral(
"trusted_origins")).toString().split(QLatin1Char(
','), QString::SkipEmptyParts);
99 if (d->formInputName.isEmpty()) {
100 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
102 d->logFailedIp = config.value(QStringLiteral(
"log_failed_ip"),
false).toBool();
103 if (d->errorMsgStashKey.isEmpty()) {
104 d->errorMsgStashKey = QStringLiteral(
"error_msg");
112 d->beforeDispatch(c);
121 d->defaultDetachTo = actionNameOrPath;
127 if (!fieldName.isEmpty()) {
128 d->formInputName = fieldName;
130 d->formInputName = QStringLiteral(DEFAULT_FORM_INPUT_NAME);
137 if (!keyName.isEmpty()) {
138 d->errorMsgStashKey = keyName;
140 d->errorMsgStashKey = QStringLiteral(
"error_msg");
147 d->ignoredNamespaces = namespaces;
153 d->useSessions = useSessions;
159 d->cookieHttpOnly = httpOnly;
165 d->cookieName = cookieName;
171 d->headerName = headerName;
177 d->genericErrorMessage = message;
183 d->genericContentType = type;
190 const QByteArray contextCookie = c->
stash(CONTEXT_CSRF_COOKIE).toByteArray();
192 if (contextCookie.isEmpty()) {
193 secret = CSRFProtectionPrivate::getNewCsrfString();
194 token = CSRFProtectionPrivate::saltCipherSecret(secret);
195 c->
setStash(CONTEXT_CSRF_COOKIE, token);
197 secret = CSRFProtectionPrivate::unsaltCipherToken(contextCookie);
198 token = CSRFProtectionPrivate::saltCipherSecret(secret);
201 c->
setStash(CONTEXT_CSRF_COOKIE_USED,
true);
211 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
215 form = QStringLiteral(
"<input type=\"hidden\" name=\"%1\" value=\"%2\" />").arg(csrf->d_ptr->formInputName, QString::fromLatin1(
CSRFProtection::getToken(c)));
222 if (CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
225 return c->
stash(CONTEXT_CSRF_CHECK_PASSED).toBool();
240 QByteArray CSRFProtectionPrivate::getNewCsrfString()
242 QByteArray csrfString;
244 while (csrfString.size() < CSRF_SECRET_LENGTH) {
245 csrfString.append(QUuid::createUuid().toRfc4122().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
248 csrfString.resize(CSRF_SECRET_LENGTH);
258 QByteArray CSRFProtectionPrivate::saltCipherSecret(
const QByteArray &secret)
261 salted.reserve(CSRF_TOKEN_LENGTH);
263 const QByteArray salt = CSRFProtectionPrivate::getNewCsrfString();
264 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
265 std::vector<std::pair<int,int>> pairs;
266 pairs.reserve(std::min(secret.size(), salt.size()));
267 for (
int i = 0; i < std::min(secret.size(), salt.size()); ++i) {
268 pairs.push_back(std::make_pair(chars.indexOf(secret.at(i)), chars.indexOf(salt.at(i))));
272 cipher.reserve(CSRF_SECRET_LENGTH);
273 for (std::size_t i = 0; i < pairs.size(); ++i) {
274 const std::pair<int,int> p = pairs.at(i);
275 cipher.append(chars[(p.first + p.second) % chars.size()]);
278 salted = salt + cipher;
289 QByteArray CSRFProtectionPrivate::unsaltCipherToken(
const QByteArray &token)
292 secret.reserve(CSRF_SECRET_LENGTH);
294 const QByteArray salt = token.left(CSRF_SECRET_LENGTH);
295 const QByteArray _token = token.mid(CSRF_SECRET_LENGTH);
297 const QByteArray chars = QByteArrayLiteral(CSRF_ALLOWED_CHARS);
298 std::vector<std::pair<int,int>> pairs;
299 pairs.reserve(std::min(salt.size(), _token.size()));
300 for (
int i = 0; i < std::min(salt.size(), _token.size()); ++i) {
301 pairs.push_back(std::make_pair(chars.indexOf(_token.at(i)), chars.indexOf(salt.at(i))));
305 for (std::size_t i = 0; i < pairs.size(); ++i) {
306 const std::pair<int,int> p = pairs.at(i);
307 int idx = p.first - p.second;
309 idx = chars.size() + idx;
311 secret.append(chars.at(idx));
322 QByteArray CSRFProtectionPrivate::getNewCsrfToken()
324 return CSRFProtectionPrivate::saltCipherSecret(CSRFProtectionPrivate::getNewCsrfString());
332 QByteArray CSRFProtectionPrivate::sanitizeToken(
const QByteArray &token)
334 QByteArray sanitized;
336 const QString tokenString = QString::fromLatin1(token);
337 if (tokenString.contains(CSRFProtectionPrivate::sanitizeRe)) {
338 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
339 }
else if (token.size() != CSRF_TOKEN_LENGTH) {
340 sanitized = CSRFProtectionPrivate::getNewCsrfToken();
352 QByteArray CSRFProtectionPrivate::getToken(
Context *c)
357 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
361 if (csrf->d_ptr->useSessions) {
362 token =
Session::value(c, QStringLiteral(CSRF_SESSION_KEY)).toByteArray();
364 QByteArray cookieToken = c->req()->
cookie(csrf->d_ptr->cookieName).toLatin1();
366 if (cookieToken.isEmpty()) {
370 token = CSRFProtectionPrivate::sanitizeToken(cookieToken);
371 if (token != cookieToken) {
372 c->
setStash(CONTEXT_CSRF_COOKIE_NEEDS_RESET,
true);
376 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from %s.", token.constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
385 void CSRFProtectionPrivate::setToken(
Context *c)
388 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
392 if (csrf->d_ptr->useSessions) {
395 QNetworkCookie cookie(csrf->d_ptr->cookieName.toLatin1(), c->
stash(CONTEXT_CSRF_COOKIE).toByteArray());
396 if (!csrf->d_ptr->cookieDomain.isEmpty()) {
397 cookie.setDomain(csrf->d_ptr->cookieDomain);
399 cookie.setExpirationDate(QDateTime::currentDateTime().addSecs(csrf->d_ptr->cookieAge));
400 cookie.setHttpOnly(csrf->d_ptr->cookieHttpOnly);
401 cookie.setPath(csrf->d_ptr->cookiePath);
402 cookie.setSecure(csrf->d_ptr->cookieSecure);
407 qCDebug(C_CSRFPROTECTION,
"Set token \"%s\" to %s.", c->
stash(CONTEXT_CSRF_COOKIE).toByteArray().constData(), csrf->d_ptr->useSessions ?
"session" :
"cookie");
415 void CSRFProtectionPrivate::reject(
Context *c,
const QString &logReason,
const QString &displayReason)
417 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
false);
420 qCCritical(C_CSRFPROTECTION) <<
"CSRFProtection plugin not registered";
424 qCWarning(C_CSRFPROTECTION,
"Forbidden: (%s): /%s [%s]", qPrintable(logReason), qPrintable(c->req()->path()), csrf->d_ptr->logFailedIp ? qPrintable(c->req()->
addressString()) :
"IP logging disabled");
427 c->
setStash(csrf->d_ptr->errorMsgStashKey, displayReason);
429 QString detachToCsrf = c->action()->
attribute(QStringLiteral(
"CSRFDetachTo"));
430 if (detachToCsrf.isEmpty()) {
431 detachToCsrf = csrf->d_ptr->defaultDetachTo;
434 Action *detachToAction =
nullptr;
436 if (!detachToCsrf.isEmpty()) {
437 detachToAction = c->controller()->
actionFor(detachToCsrf);
438 if (!detachToAction) {
441 if (!detachToAction) {
442 qCWarning(C_CSRFPROTECTION,
"Can not find action for \"%s\" to detach to.", qPrintable(detachToCsrf));
446 if (detachToAction) {
447 c->
detach(detachToAction);
449 if (!csrf->d_ptr->genericErrorMessage.isEmpty()) {
450 c->
res()->
setBody(csrf->d_ptr->genericErrorMessage);
453 const QString title = c->
translate(
"Cutelyst::CSRFProtection",
"403 Forbidden - CSRF protection check failed");
454 c->
res()->
setBody(QStringLiteral(
"<!DOCTYPE html>\n"
455 "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
457 " <title>") + title +
458 QStringLiteral(
"</title>\n"
462 QStringLiteral(
"</h1>\n"
463 " <p>") + displayReason +
464 QStringLiteral(
"</p>\n"
473 void CSRFProtectionPrivate::accept(
Context *c)
475 c->
setStash(CONTEXT_CSRF_CHECK_PASSED,
true);
476 c->
setStash(CONTEXT_CSRF_PROCESSING_DONE,
true);
483 bool CSRFProtectionPrivate::compareSaltedTokens(
const QByteArray &t1,
const QByteArray &t2)
485 const QByteArray _t1 = CSRFProtectionPrivate::unsaltCipherToken(t1);
486 const QByteArray _t2 = CSRFProtectionPrivate::unsaltCipherToken(t2);
489 int diff = _t1.size() ^ _t2.size();
490 for (
int i = 0; i < _t1.size() && i < _t2.size(); i++) {
491 diff |= _t1[i] ^ _t2[i];
500 void CSRFProtectionPrivate::beforeDispatch(
Context *c)
503 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRFProtection plugin not registered"), c->
translate(
"Cutelyst::CSRFProtection",
"The CSRF protection plugin has not been registered."));
507 const QByteArray csrfToken = CSRFProtectionPrivate::getToken(c);
508 if (!csrfToken.isNull()) {
509 c->
setStash(CONTEXT_CSRF_COOKIE, csrfToken);
514 if (c->
stash(CONTEXT_CSRF_PROCESSING_DONE).toBool()) {
518 if (c->action()->
attributes().contains(QStringLiteral(
"CSRFIgnore"))) {
519 qCDebug(C_CSRFPROTECTION,
"Action \"%s::%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
className()), qPrintable(c->action()->
reverse()));
523 if (csrf->d_ptr->ignoredNamespaces.contains(c->action()->
ns())) {
524 if (!c->action()->
attributes().contains(QStringLiteral(
"CSRFRequire"))) {
525 qCDebug(C_CSRFPROTECTION,
"Namespace \"%s\" is ignored by the CSRF protection.", qPrintable(c->action()->
ns()));
532 if (!CSRFProtectionPrivate::secureMethods.contains(c->req()->method())) {
547 if (c->req()->secure()) {
548 const QString referer = c->req()->headers().referer();
550 if (Q_UNLIKELY(referer.isEmpty())) {
551 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - no Referer."), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - no Referer."));
554 const QUrl refererUrl(referer);
555 if (Q_UNLIKELY(!refererUrl.isValid())) {
556 CSRFProtectionPrivate::reject(c, QStringLiteral(
"Referer checking failed - Referer is malformed."), c->
translate(
"Cutelyst::CSRFProtection",
"Referer checking failed - Referer is malformed."));
559 if (Q_UNLIKELY(refererUrl.scheme() != QLatin1String(
"https"))) {
560 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."));
566 const QUrl uri = c->req()->uri();
568 if (!csrf->d_ptr->useSessions) {
569 goodReferer = csrf->d_ptr->cookieDomain;
571 if (goodReferer.isEmpty()) {
572 goodReferer = uri.host();
574 const int serverPort = uri.port(c->req()->secure() ? 443 : 80);
575 if ((serverPort != 80) && (serverPort != 443)) {
576 goodReferer += QLatin1Char(
':') + QString::number(serverPort);
579 QStringList goodHosts = csrf->d_ptr->trustedOrigins;
580 goodHosts.append(goodReferer);
582 QString refererHost = refererUrl.host();
583 const int refererPort = refererUrl.port(refererUrl.scheme() == QLatin1String(
"https") ? 443 : 80);
584 if ((refererPort != 80) && (refererPort != 443)) {
585 refererHost += QLatin1Char(
':') + QString::number(refererPort);
588 bool refererCheck =
false;
589 for (
int i = 0; i < goodHosts.size(); ++i) {
590 const QString host = goodHosts.at(i);
591 if ((host.startsWith(QLatin1Char(
'.')) && (refererHost.endsWith(host) || (refererHost == host.mid(1)))) || host == refererHost) {
597 if (Q_UNLIKELY(!refererCheck)) {
599 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));
607 if (Q_UNLIKELY(csrfToken.isEmpty())) {
608 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF cookie not set."), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF cookie not set."));
612 QByteArray requestCsrfToken;
614 if (c->req()->method() != QLatin1String(
"DELETE")) {
615 if (c->req()->contentType() == QLatin1String(
"multipart/form-data")) {
617 Upload *upload = c->req()->
upload(csrf->d_ptr->formInputName);
618 if (upload && upload->
size() < 1024 ) {
619 requestCsrfToken = upload->readAll();
622 requestCsrfToken = c->req()->
bodyParam(csrf->d_ptr->formInputName).toLatin1();
625 if (requestCsrfToken.isEmpty()) {
626 requestCsrfToken = c->req()->
header(csrf->d_ptr->headerName).toLatin1();
627 if (Q_LIKELY(!requestCsrfToken.isEmpty())) {
628 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from HTTP header %s.", requestCsrfToken.constData(), qPrintable(csrf->d_ptr->headerName));
630 qCDebug(C_CSRFPROTECTION,
"Can not get token from HTTP header or form field.");
633 qCDebug(C_CSRFPROTECTION,
"Got token \"%s\" from form field %s.", requestCsrfToken.constData(), qPrintable(csrf->d_ptr->formInputName));
636 requestCsrfToken = CSRFProtectionPrivate::sanitizeToken(requestCsrfToken);
638 if (Q_UNLIKELY(!CSRFProtectionPrivate::compareSaltedTokens(requestCsrfToken, csrfToken))) {
639 CSRFProtectionPrivate::reject(c, QStringLiteral(
"CSRF token missing or incorrect."), c->
translate(
"Cutelyst::CSRFProtection",
"CSRF token missing or incorrect."));
646 CSRFProtectionPrivate::accept(c);
653 if (!c->
stash(CONTEXT_CSRF_COOKIE_NEEDS_RESET).toBool()) {
654 if (c->
stash(CONTEXT_CSRF_COOKIE_SET).toBool()) {
659 if (!c->
stash(CONTEXT_CSRF_COOKIE_USED).toBool()) {
663 CSRFProtectionPrivate::setToken(c);
664 c->
setStash(CONTEXT_CSRF_COOKIE_SET,
true);
667 #include "moc_csrfprotection.cpp"