CVE-2023-31133: Insufficient Information (NVD-CWE-noinfo)
value: Ghost is an app for new-media creators with tools to build a website, publish content, send newsletters, and offer paid subscriptions to members. Prior to version 5.46.1, due to a lack of validation when filtering on the public API endpoints, it is possible to reveal private fields via a brute force attack.

Ghost(Pro) has already been patched. Maintainers can find no evidence that the issue was exploited on Ghost(Pro) prior to the patch being added. Self-hosters are impacted if running Ghost a version below v5.46.1. v5.46.1 contains a fix for this issue. As a workaround, add a block for requests to `/ghost/api/content/*` where the `filter` query parameter contains `password` or `email`.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 7.5
Published: 2023-05-08T21:15Z
Repository: https://github.com/TryGhost/Ghost
Language: JavaScript
Files changed: ghost/core/core/server/api/endpoints/authors-public.js, ghost/core/core/server/api/endpoints/pages-public.js, ghost/core/core/server/api/endpoints/posts-public.js, ghost/core/test/regression/api/content/authors.test.js, ghost/core/test/regression/api/content/pages.test.js, ghost/core/test/regression/api/content/posts.test.js, ghost/core/test/utils/index.js
Commit date: 2023-05-03 08:43:20 -0400
Commit message: 🔒 Fixed filtering on private Author fields in Content API

refs https://github.com/TryGhost/Ghost/security/advisories/GHSA-r97q-ghch-82j9

Because our filtering layer is so coupled to the DB and we don't generally
apply restrictions, it was possible to fetch authors and filter by their
password or email field. Coupled with the "starts with" operator this can be
used to brute force the first character of these fields by trying random
combinations until an author is included in the filter. After which the next
character can be brute forced, and so on until the data has been leaked
completely.

Diff: diff --git a/ghost/core/core/server/api/endpoints/authors-public.js b/ghost/core/core/server/api/endpoints/authors-public.js
index 01b7027988..31d031d4cc 100644
--- a/ghost/core/core/server/api/endpoints/authors-public.js
+++ b/ghost/core/core/server/api/endpoints/authors-public.js
@@ -1,15 +1,27 @@
 const Promise = require('bluebird');
 const tpl = require('@tryghost/tpl');
 const errors = require('@tryghost/errors');
+const {mapQuery} = require('@tryghost/mongo-utils');
 const models = require('../../models');
 const ALLOWED_INCLUDES = ['count.posts'];
 
 const messages = {
     notFound: 'Author not found.'
 };
 
+const rejectPrivateFieldsTransformer = input => mapQuery(input, function (value, key) {
+    const lowerCaseKey = key.toLowerCase();
+    if (lowerCaseKey.startsWith('password') || lowerCaseKey.startsWith('email')) {
+        return;
+    }
+
+    return {
+        [key]: value
+    };
+});
+
 module.exports = {
     docName: 'authors',
 
     browse: {
         options: [
@@ -27,11 +39,15 @@ module.exports = {
                 }
             }
         },
         permissions: true,
         query(frame) {
-            return models.Author.findPage(frame.options);
+            const options = {
+                ...frame.options,
+                mongoTransformer: rejectPrivateFieldsTransformer
+            };
+            return models.Author.findPage(options);
         }
     },
 
     read: {
         options: [
@@ -52,11 +68,15 @@ module.exports = {
                 }
             }
         },
         permissions: true,
         query(frame) {
-            return models.Author.findOne(frame.data, frame.options)
+            const options = {
+                ...frame.options,
+                mongoTransformer: rejectPrivateFieldsTransformer
+            };
+            return models.Author.findOne(frame.data, options)
                 .then((model) => {
                     if (!model) {
                         return Promise.reject(new errors.NotFoundError({
                             message: tpl(messages.notFound)
                         }));
diff --git a/ghost/core/core/server/api/endpoints/pages-public.js b/ghost/core/core/server/api/endpoints/pages-public.js
index a3ffe3a366..bc5bbecd93 100644
--- a/ghost/core/core/server/api/endpoints/pages-public.js
+++ b/ghost/core/core/server/api/endpoints/pages-public.js
@@ -1,15 +1,27 @@
 const tpl = require('@tryghost/tpl');
 const errors = require('@tryghost/errors');
+const {mapQuery} = require('@tryghost/mongo-utils');
 const models = require('../../models');
 
 const ALLOWED_INCLUDES = ['tags', 'authors', 'tiers'];
 
 const messages = {
     pageNotFound: 'Page not found.'
 };
 
+const rejectPrivateFieldsTransformer = input => mapQuery(input, function (value, key) {
+    let lowerCaseKey = key.toLowerCase();
+    if (lowerCaseKey.startsWith('authors.password') || lowerCaseKey.startsWith('authors.email')) {
+        return;
+    }
+
+    return {
+        [key]: value
+    };
+});
+
 module.exports = {
     docName: 'pages',
 
     browse: {
         options: [
@@ -33,11 +45,15 @@ module.exports = {
                 }
             }
         },
         permissions: true,
         query(frame) {
-            return models.Post.findPage(frame.options);
+            const options = {
+                ...frame.options,
+                mongoTransformer: rejectPrivateFieldsTransformer
+            };
+            return models.Post.findPage(options);
         }
     },
 
     read: {
         options: [
@@ -62,11 +78,15 @@ module.exports = {
                 }
             }
         },
         permissions: true,
         query(frame) {
-            return models.Post.findOne(frame.data, frame.options)
+            const options = {
+                ...frame.options,
+                mongoTransformer: rejectPrivateFieldsTransformer
+            };
+            return models.Post.findOne(frame.data, options)
                 .then((model) => {
                     if (!model) {
                         throw new errors.NotFoundError({
                             message: tpl(messages.pageNotFound)
                         });
diff --git a/ghost/core/core/server/api/endpoints/posts-public.js b/ghost/core/core/server/api/endpoints/posts-public.js
index e63bbef52a..2ee9f34195 100644
--- a/ghost/core/core/server/api/endpoints/posts-public.js
+++ b/ghost/core/core/server/api/endpoints/posts-public.js
@@ -1,16 +1,28 @@
 const models = require('../../models');
 const tpl = require('@tryghost/tpl');
 const errors = require('@tryghost/errors');
+const {mapQuery} = require('@tryghost/mongo-utils');
 const postsPublicService = require('../../services/posts-public');
 
 const allowedIncludes = ['tags', 'authors', 'tiers', 'sentiment'];
 
 const messages = {
     postNotFound: 'Post not found.'
 };
 
+const rejectPrivateFieldsTransformer = input => mapQuery(input, function (value, key) {
+    const lowerCaseKey = key.toLowerCase();
+    if (lowerCaseKey.startsWith('authors.password') || lowerCaseKey.startsWith('authors.email')) {
+        return;
+    }
+
+    return {
+        [key]: value
+    };
+});
+
 module.exports = {
     docName: 'posts',
 
     browse: {
         cache: postsPublicService.api?.cache,
@@ -35,11 +47,15 @@ module.exports = {
                 }
             }
         },
         permissions: true,
         query(frame) {
-            return models.Post.findPage(frame.options);
+            const options = {
+                ...frame.options,
+                mongoTransformer: rejectPrivateFieldsTransformer
+            };
+            return models.Post.findPage(options);
         }
     },
 
     read: {
         options: [
@@ -64,11 +80,15 @@ module.exports = {
                 }
             }
         },
         permissions: true,
         query(frame) {
-            return models.Post.findOne(frame.data, frame.options)
+            const options = {
+                ...frame.options,
+                mongoTransformer: rejectPrivateFieldsTransformer
+            };
+            return models.Post.findOne(frame.data, options)
                 .then((model) => {
                     if (!model) {
                         throw new errors.NotFoundError({
                             message: tpl(messages.postNotFound)
                         });
diff --git a/ghost/core/test/regression/api/content/authors.test.js b/ghost/core/test/regression/api/content/authors.test.js
index c72764e24e..6f4ff452d7 100644
--- a/ghost/core/test/regression/api/content/authors.test.js
+++ b/ghost/core/test/regression/api/content/authors.test.js
@@ -17,10 +17,89 @@ describe('Authors Content API', function () {
 
     afterEach(async function () {
         await configUtils.restore();
     });
 
+    it('can not filter authors by password', async function () {
+        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
+        const userId = '644fd18ca1f0b764b0279b2d';
+
+        await testUtils.knex('users').insert({
+            id: userId,
+            slug: 'brute-force-password-test-user',
+            name: 'Brute Force Password Test User',
+            email: 'bruteforcepasswordtestuser@example.com',
+            password: hashedPassword,
+            status: 'active',
+            created_at: '2019-01-01 00:00:00',
+            created_by: '1'
+        });
+
+        const {id: postId} = await testUtils.knex('posts').first('id').where('slug', 'welcome');
+
+        await testUtils.knex('posts_authors').insert({
+            id: '644fd18ca1f0b764b0279b2f',
+            post_id: postId,
+            author_id: userId
+        });
+
+        const res = await request.get(localUtils.API.getApiQuery(`authors/?key=${validKey}&filter=password:'${hashedPassword}'`))
+            .set('Origin', testUtils.API.getURL())
+            .expect('Content-Type', /json/)
+            .expect('Cache-Control', testUtils.cacheRules.public)
+            .expect(200);
+
+        const data = JSON.parse(res.text);
+
+        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
+        await testUtils.knex('users').where('id', userId).del();
+
+        if (data.authors.length === 1) {
+            throw new Error('fuck');
+        }
+    });
+
+    it('can not filter authors by email', async function () {
+        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
+        const userEmail = 'bruteforcepasswordtestuser@example.com';
+        const userId = '644fd18ca1f0b764b0279b2d';
+
+        await testUtils.knex('users').insert({
+            id: userId,
+            slug: 'brute-force-password-test-user',
+            name: 'Brute Force Password Test User',
+            email: userEmail,
+            password: hashedPassword,
+            status: 'active',
+            created_at: '2019-01-01 00:00:00',
+            created_by: '1'
+        });
+
+        const {id: postId} = await testUtils.knex('posts').first('id').where('slug', 'welcome');
+
+        await testUtils.knex('posts_authors').insert({
+            id: '644fd18ca1f0b764b0279b2f',
+            post_id: postId,
+            author_id: userId
+        });
+
+        const res = await request.get(localUtils.API.getApiQuery(`authors/?key=${validKey}&filter=email:'${userEmail}'`))
+            .set('Origin', testUtils.API.getURL())
+            .expect('Content-Type', /json/)
+            .expect('Cache-Control', testUtils.cacheRules.public)
+            .expect(200);
+
+        const data = JSON.parse(res.text);
+
+        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
+        await testUtils.knex('users').where('id', userId).del();
+
+        if (data.authors.length === 1) {
+            throw new Error('fuck');
+        }
+    });
+
     it('can read authors with fields', function () {
         return request.get(localUtils.API.getApiQuery(`authors/1/?key=${validKey}&fields=name`))
             .set('Origin', testUtils.API.getURL())
             .expect('Content-Type', /json/)
             .expect('Cache-Control', testUtils.cacheRules.public)
diff --git a/ghost/core/test/regression/api/content/pages.test.js b/ghost/core/test/regression/api/content/pages.test.js
index 423b0e6a69..e0a26c6f25 100644
--- a/ghost/core/test/regression/api/content/pages.test.js
+++ b/ghost/core/test/regression/api/content/pages.test.js
@@ -18,10 +18,89 @@ describe('api/endpoints/content/pages', function () {
 
     afterEach(async function () {
         await configUtils.restore();
     });
 
+    it('can not filter pages by author.password or authors.password', async function () {
+        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
+        const userId = '644fd18ca1f0b764b0279b2d';
+
+        await testUtils.knex('users').insert({
+            id: userId,
+            slug: 'brute-force-password-test-user',
+            name: 'Brute Force Password Test User',
+            email: 'bruteforcepasswordtestuseremail@example.com',
+            password: hashedPassword,
+            status: 'active',
+            created_at: '2019-01-01 00:00:00',
+            created_by: '1'
+        });
+
+        const {id: postId} = await testUtils.knex('posts').first('id').where('type', 'page');
+
+        await testUtils.knex('posts_authors').insert({
+            id: '644fd18ca1f0b764b0279b2f',
+            post_id: postId,
+            author_id: userId
+        });
+
+        const res = await request.get(localUtils.API.getApiQuery(`pages/?key=${key}&filter=authors.password:'${hashedPassword}'`))
+            .set('Origin', testUtils.API.getURL())
+            .expect('Content-Type', /json/)
+            .expect('Cache-Control', testUtils.cacheRules.public)
+            .expect(200);
+
+        const data = JSON.parse(res.text);
+
+        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
+        await testUtils.knex('users').where('id', userId).del();
+
+        if (data.pages.length === 1) {
+            throw new Error('fuck');
+        }
+    });
+
+    it('can not filter pages by author.email or authors.email', async function () {
+        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
+        const userEmail = 'bruteforcepasswordtestuseremail@example.com';
+        const userId = '644fd18ca1f0b764b0279b2d';
+
+        await testUtils.knex('users').insert({
+            id: userId,
+            slug: 'brute-force-password-test-user',
+            name: 'Brute Force Password Test User',
+            email: userEmail,
+            password: hashedPassword,
+            status: 'active',
+            created_at: '2019-01-01 00:00:00',
+            created_by: '1'
+        });
+
+        const {id: postId} = await testUtils.knex('posts').first('id').where('type', 'page');
+
+        await testUtils.knex('posts_authors').insert({
+            id: '644fd18ca1f0b764b0279b2f',
+            post_id: postId,
+            author_id: userId
+        });
+
+        const res = await request.get(localUtils.API.getApiQuery(`pages/?key=${key}&filter=authors.email:'${userEmail}'`))
+            .set('Origin', testUtils.API.getURL())
+            .expect('Content-Type', /json/)
+            .expect('Cache-Control', testUtils.cacheRules.public)
+            .expect(200);
+
+        const data = JSON.parse(res.text);
+
+        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
+        await testUtils.knex('users').where('id', userId).del();
+
+        if (data.pages.length === 1) {
+            throw new Error('fuck');
+        }
+    });
+
     it('Returns a validation error when unsupported "page" filter is used', function () {
         return request.get(localUtils.API.getApiQuery(`pages/?key=${key}&filter=page:false`))
             .set('Origin', testUtils.API.getURL())
             .expect('Content-Type', /json/)
             .expect('Cache-Control', testUtils.cacheRules.private)
diff --git a/ghost/core/test/regression/api/content/posts.test.js b/ghost/core/test/regression/api/content/posts.test.js
index 597848bb94..7f86ae434f 100644
--- a/ghost/core/test/regression/api/content/posts.test.js
+++ b/ghost/core/test/regression/api/content/posts.test.js
@@ -21,10 +21,89 @@ describe('api/endpoints/content/posts', function () {
         urlUtils.restore();
     });
 
     const validKey = localUtils.getValidKey();
 
+    it('can not filter posts by author.password or authors.password', async function () {
+        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
+        const userId = '644fd18ca1f0b764b0279b2d';
+
+        await testUtils.knex('users').insert({
+            id: userId,
+            slug: 'brute-force-password-test-user',
+            name: 'Brute Force Password Test User',
+            email: 'bruteforcepasswordtestuseremail@example.com',
+            password: hashedPassword,
+            status: 'active',
+            created_at: '2019-01-01 00:00:00',
+            created_by: '1'
+        });
+
+        const {id: postId} = await testUtils.knex('posts').first('id').where('slug', 'welcome');
+
+        await testUtils.knex('posts_authors').insert({
+            id: '644fd18ca1f0b764b0279b2f',
+            post_id: postId,
+            author_id: userId
+        });
+
+        const res = await request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=authors.password:'${hashedPassword}'`))
+            .set('Origin', testUtils.API.getURL())
+            .expect('Content-Type', /json/)
+            .expect('Cache-Control', testUtils.cacheRules.public)
+            .expect(200);
+
+        const data = JSON.parse(res.text);
+
+        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
+        await testUtils.knex('users').where('id', userId).del();
+
+        if (data.posts.length === 1) {
+            throw new Error('fuck');
+        }
+    });
+
+    it('can not filter posts by author.email or authors.email', async function () {
+        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
+        const userEmail = 'bruteforcepasswordtestuseremail@example.com';
+        const userId = '644fd18ca1f0b764b0279b2d';
+
+        await testUtils.knex('users').insert({
+            id: userId,
+            slug: 'brute-force-password-test-user',
+            name: 'Brute Force Password Test User',
+            email: userEmail,
+            password: hashedPassword,
+            status: 'active',
+            created_at: '2019-01-01 00:00:00',
+            created_by: '1'
+        });
+
+        const {id: postId} = await testUtils.knex('posts').first('id').where('slug', 'welcome');
+
+        await testUtils.knex('posts_authors').insert({
+            id: '644fd18ca1f0b764b0279b2f',
+            post_id: postId,
+            author_id: userId
+        });
+
+        const res = await request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=authors.email:'${userEmail}'`))
+            .set('Origin', testUtils.API.getURL())
+            .expect('Content-Type', /json/)
+            .expect('Cache-Control', testUtils.cacheRules.public)
+            .expect(200);
+
+        const data = JSON.parse(res.text);
+
+        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
+        await testUtils.knex('users').where('id', userId).del();
+
+        if (data.posts.length === 1) {
+            throw new Error('fuck');
+        }
+    });
+
     it('browse posts', function (done) {
         request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}`))
             .set('Origin', testUtils.API.getURL())
             .expect('Content-Type', /json/)
             .expect('Cache-Control', testUtils.cacheRules.public)
diff --git a/ghost/core/test/utils/index.js b/ghost/core/test/utils/index.js
index 93c36276ea..b43d59bc6f 100644
--- a/ghost/core/test/utils/index.js
+++ b/ghost/core/test/utils/index.js
@@ -97,10 +97,11 @@ module.exports = {
     stopGhost: e2eUtils.stopGhost,
     getExistingData: e2eUtils.getExistingData,
 
     teardownDb: dbUtils.teardown,
     truncate: dbUtils.truncate,
+    knex: dbUtils.knex,
     setup: setup,
     createUser: createUser,
     createPost: createPost,
     createEmailedPost,
 

Vulnerable code:             return models.Author.findPage(frame.options);
            return models.Author.findOne(frame.data, frame.options)
            return models.Post.findPage(frame.options);
            return models.Post.findOne(frame.data, frame.options)
            return models.Post.findPage(frame.options);
            return models.Post.findOne(frame.data, frame.options)
Fixed code: const {mapQuery} = require('@tryghost/mongo-utils');
const rejectPrivateFieldsTransformer = input => mapQuery(input, function (value, key) {
    const lowerCaseKey = key.toLowerCase();
    if (lowerCaseKey.startsWith('password') || lowerCaseKey.startsWith('email')) {
        return;
    }

    return {
        [key]: value
    };
});

            const options = {
                ...frame.options,
                mongoTransformer: rejectPrivateFieldsTransformer
            };
            return models.Author.findPage(options);
            const options = {
                ...frame.options,
                mongoTransformer: rejectPrivateFieldsTransformer
            };
            return models.Author.findOne(frame.data, options)
const {mapQuery} = require('@tryghost/mongo-utils');
const rejectPrivateFieldsTransformer = input => mapQuery(input, function (value, key) {
    let lowerCaseKey = key.toLowerCase();
    if (lowerCaseKey.startsWith('authors.password') || lowerCaseKey.startsWith('authors.email')) {
        return;
    }

    return {
        [key]: value
    };
});

            const options = {
                ...frame.options,
                mongoTransformer: rejectPrivateFieldsTransformer
            };
            return models.Post.findPage(options);
            const options = {
                ...frame.options,
                mongoTransformer: rejectPrivateFieldsTransformer
            };
            return models.Post.findOne(frame.data, options)
const {mapQuery} = require('@tryghost/mongo-utils');
const rejectPrivateFieldsTransformer = input => mapQuery(input, function (value, key) {
    const lowerCaseKey = key.toLowerCase();
    if (lowerCaseKey.startsWith('authors.password') || lowerCaseKey.startsWith('authors.email')) {
        return;
    }

    return {
        [key]: value
    };
});

            const options = {
                ...frame.options,
                mongoTransformer: rejectPrivateFieldsTransformer
            };
            return models.Post.findPage(options);
            const options = {
                ...frame.options,
                mongoTransformer: rejectPrivateFieldsTransformer
            };
            return models.Post.findOne(frame.data, options)
    it('can not filter authors by password', async function () {
        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
        const userId = '644fd18ca1f0b764b0279b2d';

        await testUtils.knex('users').insert({
            id: userId,
            slug: 'brute-force-password-test-user',
            name: 'Brute Force Password Test User',
            email: 'bruteforcepasswordtestuser@example.com',
            password: hashedPassword,
            status: 'active',
            created_at: '2019-01-01 00:00:00',
            created_by: '1'
        });

        const {id: postId} = await testUtils.knex('posts').first('id').where('slug', 'welcome');

        await testUtils.knex('posts_authors').insert({
            id: '644fd18ca1f0b764b0279b2f',
            post_id: postId,
            author_id: userId
        });

        const res = await request.get(localUtils.API.getApiQuery(`authors/?key=${validKey}&filter=password:'${hashedPassword}'`))
            .set('Origin', testUtils.API.getURL())
            .expect('Content-Type', /json/)
            .expect('Cache-Control', testUtils.cacheRules.public)
            .expect(200);

        const data = JSON.parse(res.text);

        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
        await testUtils.knex('users').where('id', userId).del();

        if (data.authors.length === 1) {
            throw new Error('fuck');
        }
    });

    it('can not filter authors by email', async function () {
        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
        const userEmail = 'bruteforcepasswordtestuser@example.com';
        const userId = '644fd18ca1f0b764b0279b2d';

        await testUtils.knex('users').insert({
            id: userId,
            slug: 'brute-force-password-test-user',
            name: 'Brute Force Password Test User',
            email: userEmail,
            password: hashedPassword,
            status: 'active',
            created_at: '2019-01-01 00:00:00',
            created_by: '1'
        });

        const {id: postId} = await testUtils.knex('posts').first('id').where('slug', 'welcome');

        await testUtils.knex('posts_authors').insert({
            id: '644fd18ca1f0b764b0279b2f',
            post_id: postId,
            author_id: userId
        });

        const res = await request.get(localUtils.API.getApiQuery(`authors/?key=${validKey}&filter=email:'${userEmail}'`))
            .set('Origin', testUtils.API.getURL())
            .expect('Content-Type', /json/)
            .expect('Cache-Control', testUtils.cacheRules.public)
            .expect(200);

        const data = JSON.parse(res.text);

        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
        await testUtils.knex('users').where('id', userId).del();

        if (data.authors.length === 1) {
            throw new Error('fuck');
        }
    });

    it('can not filter pages by author.password or authors.password', async function () {
        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
        const userId = '644fd18ca1f0b764b0279b2d';

        await testUtils.knex('users').insert({
            id: userId,
            slug: 'brute-force-password-test-user',
            name: 'Brute Force Password Test User',
            email: 'bruteforcepasswordtestuseremail@example.com',
            password: hashedPassword,
            status: 'active',
            created_at: '2019-01-01 00:00:00',
            created_by: '1'
        });

        const {id: postId} = await testUtils.knex('posts').first('id').where('type', 'page');

        await testUtils.knex('posts_authors').insert({
            id: '644fd18ca1f0b764b0279b2f',
            post_id: postId,
            author_id: userId
        });

        const res = await request.get(localUtils.API.getApiQuery(`pages/?key=${key}&filter=authors.password:'${hashedPassword}'`))
            .set('Origin', testUtils.API.getURL())
            .expect('Content-Type', /json/)
            .expect('Cache-Control', testUtils.cacheRules.public)
            .expect(200);

        const data = JSON.parse(res.text);

        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
        await testUtils.knex('users').where('id', userId).del();

        if (data.pages.length === 1) {
            throw new Error('fuck');
        }
    });

    it('can not filter pages by author.email or authors.email', async function () {
        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
        const userEmail = 'bruteforcepasswordtestuseremail@example.com';
        const userId = '644fd18ca1f0b764b0279b2d';

        await testUtils.knex('users').insert({
            id: userId,
            slug: 'brute-force-password-test-user',
            name: 'Brute Force Password Test User',
            email: userEmail,
            password: hashedPassword,
            status: 'active',
            created_at: '2019-01-01 00:00:00',
            created_by: '1'
        });

        const {id: postId} = await testUtils.knex('posts').first('id').where('type', 'page');

        await testUtils.knex('posts_authors').insert({
            id: '644fd18ca1f0b764b0279b2f',
            post_id: postId,
            author_id: userId
        });

        const res = await request.get(localUtils.API.getApiQuery(`pages/?key=${key}&filter=authors.email:'${userEmail}'`))
            .set('Origin', testUtils.API.getURL())
            .expect('Content-Type', /json/)
            .expect('Cache-Control', testUtils.cacheRules.public)
            .expect(200);

        const data = JSON.parse(res.text);

        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
        await testUtils.knex('users').where('id', userId).del();

        if (data.pages.length === 1) {
            throw new Error('fuck');
        }
    });

    it('can not filter posts by author.password or authors.password', async function () {
        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
        const userId = '644fd18ca1f0b764b0279b2d';

        await testUtils.knex('users').insert({
            id: userId,
            slug: 'brute-force-password-test-user',
            name: 'Brute Force Password Test User',
            email: 'bruteforcepasswordtestuseremail@example.com',
            password: hashedPassword,
            status: 'active',
            created_at: '2019-01-01 00:00:00',
            created_by: '1'
        });

        const {id: postId} = await testUtils.knex('posts').first('id').where('slug', 'welcome');

        await testUtils.knex('posts_authors').insert({
            id: '644fd18ca1f0b764b0279b2f',
            post_id: postId,
            author_id: userId
        });

        const res = await request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=authors.password:'${hashedPassword}'`))
            .set('Origin', testUtils.API.getURL())
            .expect('Content-Type', /json/)
            .expect('Cache-Control', testUtils.cacheRules.public)
            .expect(200);

        const data = JSON.parse(res.text);

        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
        await testUtils.knex('users').where('id', userId).del();

        if (data.posts.length === 1) {
            throw new Error('fuck');
        }
    });

    it('can not filter posts by author.email or authors.email', async function () {
        const hashedPassword = '$2a$10$FxFlCsNBgXw42cBj0l1GFu39jffibqTqyAGBz7uCLwetYAdBYJEe6';
        const userEmail = 'bruteforcepasswordtestuseremail@example.com';
        const userId = '644fd18ca1f0b764b0279b2d';

        await testUtils.knex('users').insert({
            id: userId,
            slug: 'brute-force-password-test-user',
            name: 'Brute Force Password Test User',
            email: userEmail,
            password: hashedPassword,
            status: 'active',
            created_at: '2019-01-01 00:00:00',
            created_by: '1'
        });

        const {id: postId} = await testUtils.knex('posts').first('id').where('slug', 'welcome');

        await testUtils.knex('posts_authors').insert({
            id: '644fd18ca1f0b764b0279b2f',
            post_id: postId,
            author_id: userId
        });

        const res = await request.get(localUtils.API.getApiQuery(`posts/?key=${validKey}&filter=authors.email:'${userEmail}'`))
            .set('Origin', testUtils.API.getURL())
            .expect('Content-Type', /json/)
            .expect('Cache-Control', testUtils.cacheRules.public)
            .expect(200);

        const data = JSON.parse(res.text);

        await testUtils.knex('posts_authors').where('id', '644fd18ca1f0b764b0279b2f').del();
        await testUtils.knex('users').where('id', userId).del();

        if (data.posts.length === 1) {
            throw new Error('fuck');
        }
    });

    knex: dbUtils.knex,