1 #include "coverageobject.h"
2 
3 #include <Cutelyst/Application>
4 #include <Cutelyst/Controller>
5 #include <Cutelyst/Plugins/CSRFProtection/CSRFProtection>
6 
7 #include <QObject>
8 #include <QTest>
9 #include <QNetworkCookie>
10 
11 using namespace Cutelyst;
12 
13 class TestCsrfProtection : public CoverageObject
14 {
15     Q_OBJECT
16 public:
TestCsrfProtection(QObject * parent=nullptr)17     explicit TestCsrfProtection(QObject *parent = nullptr) : CoverageObject(parent) {}
18 
19     void initTest();
20     void cleanupTest();
21 
22 private Q_SLOTS:
23     void initTestCase();
24 
25     void doTest_data();
26     void doTest();
27     void detachToOnArgument();
28     void csrfIgnorArgument();
29     void ignoreNamespace();
30     void ignoreNamespaceRequired();
31     void csrfRedirect();
32 
33     void cleanupTestCase();
34 
35 private:
36     TestEngine *m_engine;
37     QNetworkCookie m_cookie;
38     const QString m_cookieName = QStringLiteral("xsrftoken");
39     const QString m_fieldName = QStringLiteral("xsrfprotect");
40     const QString m_headerName = QStringLiteral("X-MY-CSRF");
41     QString m_fieldValue;
42 
43     TestEngine *getEngine();
44 
45     void performTest();
46 };
47 
48 class CsrfprotectionTest : public Controller
49 {
50     Q_OBJECT
51 public:
CsrfprotectionTest(QObject * parent)52     explicit CsrfprotectionTest(QObject *parent) : Controller(parent) {}
53 
54     C_ATTR(testCsrf, :Local :AutoArgs)
testCsrf(Context * c)55     void testCsrf(Context *c)
56     {
57         c->res()->setContentType(QStringLiteral("text/plain"));
58         if (c->req()->isGet()) {
59             c->res()->setBody(CSRFProtection::getToken(c));
60         } else {
61             c->res()->setBody(QByteArrayLiteral("allowed"));
62         }
63     }
64 
65     C_ATTR(testCsrfIgnore, :Local :AutoArgs :CSRFIgnore)
testCsrfIgnore(Context * c)66     void testCsrfIgnore(Context *c)
67     {
68         c->res()->setContentType(QStringLiteral("text/plain"));
69         c->res()->setBody(QByteArrayLiteral("allowed"));
70     }
71 
72     C_ATTR(testCsrfRedirect, :Local :AutoArgs)
testCsrfRedirect(Context * c)73     void testCsrfRedirect(Context *c)
74     {
75         c->res()->redirect(QStringLiteral("http//www.example.com"));
76     }
77 
78     C_ATTR(testCsrfDetachTo, :Local :AutoArgs :CSRFDetachTo(csrfdenied))
testCsrfDetachTo(Context * c)79     void testCsrfDetachTo(Context *c)
80     {
81         c->res()->setContentType(QStringLiteral("text/plain"));
82         c->res()->setBody(QByteArrayLiteral("allowed"));
83     }
84 
85     C_ATTR(csrfdenied, :Private :AutoArgs)
csrfdenied(Context * c)86     void csrfdenied(Context *c)
87     {
88         c->res()->setContentType(QStringLiteral("text/plain"));
89         c->res()->setBody(QByteArrayLiteral("detachdenied"));
90     }
91 };
92 
93 class CsrfprotectionNsTest : public Controller
94 {
95     Q_OBJECT
96     C_NAMESPACE("testns")
97 public:
CsrfprotectionNsTest(QObject * parent)98     explicit CsrfprotectionNsTest(QObject *parent) : Controller(parent) {}
99 
100     C_ATTR(testCsrf, :Local :AutoArgs)
testCsrf(Context * c)101     void testCsrf(Context *c)
102     {
103         c->res()->setContentType(QStringLiteral("text/plain"));
104         c->res()->setBody(QByteArrayLiteral("allowed"));
105     }
106 
107     C_ATTR(testCsrfRequired, :Local :AutoArgs :CSRFRequire)
testCsrfRequired(Context * c)108     void testCsrfRequired(Context *c)
109     {
110         c->res()->setContentType(QStringLiteral("text/plain"));
111         c->res()->setBody(QByteArrayLiteral("allowed"));
112     }
113 };
114 
initTestCase()115 void TestCsrfProtection::initTestCase()
116 {
117     m_engine = getEngine();
118     QVERIFY(m_engine);
119     if (m_cookie.value().isEmpty()) {
120         const QVariantMap result = m_engine->createRequest(QStringLiteral("GET"), QStringLiteral("csrfprotection/test/testCsrf"), QByteArray(), Headers(), nullptr);
121         const QList<QNetworkCookie> cookies = QNetworkCookie::parseCookies(result.value(QStringLiteral("headers")).value<Headers>().header(QStringLiteral("Set-Cookie")).toLatin1());
122         QVERIFY(!cookies.empty());
123         for (const QNetworkCookie &cookie : cookies) {
124             if (cookie.name() == m_cookieName.toLatin1()) {
125                 m_cookie = cookie;
126                 break;
127             }
128         }
129         QVERIFY(!m_cookie.value().isEmpty());
130         initTest();
131     }
132 }
133 
getEngine()134 TestEngine* TestCsrfProtection::getEngine()
135 {
136     qputenv("RECURSION", QByteArrayLiteral("100"));
137     auto app = new TestApplication;
138     auto engine = new TestEngine(app, QVariantMap());
139     auto csrf = new CSRFProtection(app);
140     csrf->setCookieName(m_cookieName);
141     csrf->setGenericErrorMessage(QStringLiteral("denied"));
142     csrf->setFormFieldName(m_fieldName);
143     csrf->setHeaderName(m_headerName);
144     csrf->setIgnoredNamespaces(QStringList(QStringLiteral("testns")));
145     new CsrfprotectionTest(app);
146     new CsrfprotectionNsTest(app);
147     if (!engine->init()) {
148         delete engine;
149         return nullptr;
150     }
151     return engine;
152 }
153 
cleanupTestCase()154 void TestCsrfProtection::cleanupTestCase()
155 {
156     delete m_engine;
157 }
158 
initTest()159 void TestCsrfProtection::initTest()
160 {
161     Headers headers;
162     headers.setHeader(QStringLiteral("Cookie"), QString::fromLatin1(m_cookie.toRawForm(QNetworkCookie::NameAndValueOnly)));
163     m_fieldValue = m_engine->createRequest(QStringLiteral("GET"), QStringLiteral("csrfprotection/test/testCsrf"), QByteArray(), headers, nullptr).value(QStringLiteral("body")).toString();
164 }
165 
cleanupTest()166 void TestCsrfProtection::cleanupTest()
167 {
168     m_fieldValue.clear();
169 }
170 
doTest()171 void TestCsrfProtection::doTest()
172 {
173     QFETCH(QString, method);
174     QFETCH(Headers, headers);
175     QFETCH(QByteArray, body);
176 
177     const QVariantMap result = m_engine->createRequest(method, QStringLiteral("csrfprotection/test/testCsrf"), QByteArray(), headers, &body);
178 
179     QTEST(result.value(QStringLiteral("statusCode")).value<int>(), "status");
180     QTEST(result.value(QStringLiteral("body")).toByteArray(), "output");
181 }
182 
doTest_data()183 void TestCsrfProtection::doTest_data()
184 {
185     QTest::addColumn<QString>("method");
186     QTest::addColumn<Headers>("headers");
187     QTest::addColumn<QByteArray>("body");
188     QTest::addColumn<int>("status");
189     QTest::addColumn<QByteArray>("output");
190 
191     for (const QString &method : {QStringLiteral("POST"), QStringLiteral("PUT"), QStringLiteral("PATCH"), QStringLiteral("DELETE")}) {
192         const QString cookieValid = QString::fromLatin1(m_cookie.toRawForm(QNetworkCookie::NameAndValueOnly));
193         QString cookieInvalid = cookieValid;
194         QCharRef cookieLast = cookieInvalid[cookieInvalid.size() - 1];
195         if (cookieLast.isDigit()) {
196             if (cookieLast.unicode() < 57) {
197                 cookieLast.unicode()++;
198             } else {
199                 cookieLast.unicode()--;
200             }
201         } else {
202             if (cookieLast.isUpper()) {
203                 cookieLast = cookieLast.toLower();
204             } else {
205                 cookieLast = cookieLast.toUpper();
206             }
207         }
208 
209         QString fieldValueInvalid = m_fieldValue;
210         QCharRef fieldLast = fieldValueInvalid[fieldValueInvalid.size() - 2];
211         if (fieldLast.isDigit()) {
212             if (fieldLast.unicode() < 57) {
213                 fieldLast.unicode()++;
214             } else {
215                 fieldLast.unicode()--;
216             }
217         } else {
218             if (fieldLast.isUpper()) {
219                 fieldLast = fieldLast.toLower();
220             } else {
221                 fieldLast = fieldLast.toUpper();
222             }
223         }
224 
225         const QString fieldValid = m_fieldName + QLatin1Char('=') + m_fieldValue;
226         const QString fieldInvalid = m_fieldName + QLatin1Char('=') + fieldValueInvalid;
227 
228         Headers headers;
229         headers.setContentType(QStringLiteral("application/x-www-form-urlencoded"));
230         QByteArray body;
231 
232         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(absent), header(absent), field(absent)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
233 
234         headers.setHeader(QStringLiteral("Cookie"), cookieValid);
235         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(valid), header(absent), field(absent)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
236 
237         headers.setHeader(QStringLiteral("Cookie"), cookieInvalid);
238         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(invalid), header(absent), field(absent)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
239 
240         headers.removeHeader(QStringLiteral("Cookie"));
241         headers.setHeader(m_headerName, m_fieldValue);
242         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(absent), header(valid), field(absent)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
243 
244         headers.setHeader(QStringLiteral("Cookie"), cookieValid);
245         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(valid), header(valid), field(absent)").arg(method))) << method << headers << body << 200 << QByteArrayLiteral("allowed");
246 
247         headers.setHeader(QStringLiteral("Cookie"), cookieInvalid);
248         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(invalid), header(valid), field(absent)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
249 
250         headers.setHeader(m_headerName, fieldValueInvalid);
251         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(invalid), header(invalid), field(absent)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
252 
253         headers.setHeader(QStringLiteral("Cookie"), cookieValid);
254         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(valid), header(invalid), field(absent)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
255 
256         body = (method != QLatin1String("DELETE")) ? fieldValid.toLatin1() : QByteArray();
257         int status = (method != QLatin1String("DELETE")) ? 200 : 403;
258         QByteArray result = (method != QLatin1String("DELETE")) ? QByteArrayLiteral("allowed") : QByteArrayLiteral("denied");
259         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(valid), header(invalid), field(valid)").arg(method))) << method << headers << body << status << result;
260 
261         body = fieldInvalid.toLatin1();
262         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(valid), header(invalid), field(invalid)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
263 
264         headers.setHeader(m_headerName, m_fieldValue);
265         status = (method == QLatin1String("DELETE")) ? 200 : 403;
266         result = (method == QLatin1String("DELETE")) ? QByteArrayLiteral("allowed") : QByteArrayLiteral("denied");
267         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(valid), header(valid), field(invalid)").arg(method))) << method << headers << body << status << result;
268 
269         headers.setHeader(QStringLiteral("Cookie"), cookieInvalid);
270         body = fieldValid.toLatin1();
271         QTest::newRow(qUtf8Printable(QStringLiteral("%1: cookie(invalid), header(valid), field(valid)").arg(method))) << method << headers << body << 403 << QByteArrayLiteral("denied");
272     }
273 }
274 
detachToOnArgument()275 void TestCsrfProtection::detachToOnArgument()
276 {
277     const QVariantMap result = m_engine->createRequest(QStringLiteral("POST"), QStringLiteral("csrfprotection/test/testCsrfDetachTo"), QByteArray(), Headers(), nullptr);
278     QCOMPARE(result.value(QStringLiteral("statusCode")).value<int>(), 403);
279     QCOMPARE(result.value(QStringLiteral("body")).toByteArray(), QByteArrayLiteral("detachdenied"));
280 }
281 
csrfIgnorArgument()282 void TestCsrfProtection::csrfIgnorArgument()
283 {
284     const QVariantMap result = m_engine->createRequest(QStringLiteral("POST"), QStringLiteral("csrfprotection/test/testCsrfIgnore"), QByteArray(), Headers(), nullptr);
285     QCOMPARE(result.value(QStringLiteral("statusCode")).value<int>(), 200);
286     QCOMPARE(result.value(QStringLiteral("body")).toByteArray(), QByteArrayLiteral("allowed"));
287 }
288 
ignoreNamespace()289 void TestCsrfProtection::ignoreNamespace()
290 {
291     const QVariantMap result = m_engine->createRequest(QStringLiteral("POST"), QStringLiteral("testns/testCsrf"), QByteArray(), Headers(), nullptr);
292     QCOMPARE(result.value(QStringLiteral("statusCode")).value<int>(), 200);
293     QCOMPARE(result.value(QStringLiteral("body")).toByteArray(), QByteArrayLiteral("allowed"));
294 }
295 
ignoreNamespaceRequired()296 void TestCsrfProtection::ignoreNamespaceRequired()
297 {
298     const QVariantMap result = m_engine->createRequest(QStringLiteral("POST"), QStringLiteral("testns/testCsrfRequired"), QByteArray(), Headers(), nullptr);
299     QCOMPARE(result.value(QStringLiteral("statusCode")).value<int>(), 403);
300     QCOMPARE(result.value(QStringLiteral("body")).toByteArray(), QByteArrayLiteral("denied"));
301 }
302 
csrfRedirect()303 void TestCsrfProtection::csrfRedirect()
304 {
305     const QVariantMap result = m_engine->createRequest(QStringLiteral("POST"), QStringLiteral("csrfprotection/test/testCsrfRedirect"), QByteArray(), Headers(), nullptr);
306     QCOMPARE(result.value(QStringLiteral("statusCode")).value<int>(), 403);
307     QCOMPARE(result.value(QStringLiteral("body")).toByteArray(), QByteArrayLiteral("denied"));
308 }
309 
310 QTEST_MAIN(TestCsrfProtection)
311 
312 #include "testcsrfprotection.moc"
313