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