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,6 +1,7 @@ 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']; @@ -8,6 +9,17 @@ 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', @@ -29,7 +41,11 @@ module.exports = { }, permissions: true, query(frame) { - return models.Author.findPage(frame.options); + const options = { + ...frame.options, + mongoTransformer: rejectPrivateFieldsTransformer + }; + return models.Author.findPage(options); } }, @@ -54,7 +70,11 @@ 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({ 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,5 +1,6 @@ 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']; @@ -8,6 +9,17 @@ 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', @@ -35,7 +47,11 @@ module.exports = { }, permissions: true, query(frame) { - return models.Post.findPage(frame.options); + const options = { + ...frame.options, + mongoTransformer: rejectPrivateFieldsTransformer + }; + return models.Post.findPage(options); } }, @@ -64,7 +80,11 @@ 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({ 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,6 +1,7 @@ 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']; @@ -9,6 +10,17 @@ 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', @@ -37,7 +49,11 @@ module.exports = { }, permissions: true, query(frame) { - return models.Post.findPage(frame.options); + const options = { + ...frame.options, + mongoTransformer: rejectPrivateFieldsTransformer + }; + return models.Post.findPage(options); } }, @@ -66,7 +82,11 @@ 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({ 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 @@ -19,6 +19,85 @@ describe('Authors Content API', 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()) 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 @@ -20,6 +20,85 @@ describe('api/endpoints/content/pages', 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()) 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 @@ -23,6 +23,85 @@ describe('api/endpoints/content/posts', function () { 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()) 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 @@ -99,6 +99,7 @@ module.exports = { teardownDb: dbUtils.teardown, truncate: dbUtils.truncate, + knex: dbUtils.knex, setup: setup, createUser: createUser, createPost: createPost,