0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Private RSS feed (#9088)

refs #9001

When a blog is in private mode there is now an unguessable URL that allows access to the RSS feed for internal use, commenting systems, etc.

- add public hash for private blogging
  - auto generate on bootstrap if missing
  - global hash, we can re-use in the future
- update private blogging middleware to detect the private RSS URL and rewrite it so that the normal rss route/code is used for display
- if a normal `/rss/` route is accessed with a private session return a 404
This commit is contained in:
Katharina Irrgang 2017-10-05 12:07:32 +02:00 committed by Kevin Ansfield
parent 7be165da07
commit 7800ed3d8b
4 changed files with 136 additions and 3 deletions

View file

@ -4,6 +4,7 @@ var fs = require('fs'),
path = require('path'),
config = require('../../../config'),
utils = require('../../../utils'),
errors = require('../../../errors'),
i18n = require('../../../i18n'),
settingsCache = require('../../../settings/cache'),
privateRoute = '/' + config.get('routeKeywords').private + '/',
@ -57,7 +58,25 @@ privateBlogging = {
});
}
privateBlogging.authenticatePrivateSession(req, res, next);
// CASE: Allow private RSS feed urls.
// Any url which contains the hash and the postfix /rss is allowed to access a private rss feed without
// a session. As soon as a path matches, we rewrite the url. Even Express uses rewriting when using `app.use()`.
if (req.url.indexOf(settingsCache.get('public_hash') + '/rss') !== -1) {
req.url = req.url.replace(settingsCache.get('public_hash') + '/', '');
return next();
}
// NOTE: Redirect to /private if the session does not exist.
privateBlogging.authenticatePrivateSession(req, res, function onSessionVerified() {
// CASE: RSS is disabled for private blogging e.g. they create overhead
if (req.path.lastIndexOf('/rss/', 0) === 0 || req.path.lastIndexOf('/rss/') === req.url.length - 5) {
return next(new errors.NotFoundError({
message: i18n.t('errors.errors.pageNotFound')
}));
}
next();
});
},
authenticatePrivateSession: function authenticatePrivateSession(req, res, next) {

View file

@ -2,8 +2,9 @@
var should = require('should'), // jshint ignore:line
sinon = require('sinon'),
crypto = require('crypto'),
settingsCache = require('../../../settings/cache'),
fs = require('fs'),
errors = require('../../../errors'),
settingsCache = require('../../../settings/cache'),
privateBlogging = require('../lib/middleware'),
sandbox = sinon.sandbox.create();
@ -223,6 +224,114 @@ describe('Private Blogging', function () {
privateBlogging.authenticateProtection(req, res, next);
res.redirect.called.should.be.true();
});
it('filterPrivateRoutes should 404 for /rss/ requests', function () {
var salt = Date.now().toString();
req.url = req.path = '/rss/';
req.session = {
token: hash('rightpassword', salt),
salt: salt
};
res.isPrivateBlog = true;
res.redirect = sandbox.spy();
privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
(next.firstCall.args[0] instanceof errors.NotFoundError).should.eql(true);
});
it('filterPrivateRoutes should 404 for /rss requests', function () {
var salt = Date.now().toString();
req.url = req.path = '/rss';
req.session = {
token: hash('rightpassword', salt),
salt: salt
};
res.isPrivateBlog = true;
res.redirect = sandbox.spy();
privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
(next.firstCall.args[0] instanceof errors.NotFoundError).should.eql(true);
});
it('filterPrivateRoutes should 404 for tag rss requests', function () {
var salt = Date.now().toString();
req.url = req.path = '/tag/welcome/rss/';
req.session = {
token: hash('rightpassword', salt),
salt: salt
};
res.isPrivateBlog = true;
res.redirect = sandbox.spy();
privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
(next.firstCall.args[0] instanceof errors.NotFoundError).should.eql(true);
});
it('filterPrivateRoutes: allow private /rss/ feed', function () {
settingsStub.withArgs('public_hash').returns('777aaa');
req.url = req.originalUrl = req.path = '/777aaa/rss/';
req.params = {};
res.isPrivateBlog = true;
res.locals = {};
privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
req.url.should.eql('/rss/');
});
it('filterPrivateRoutes: allow private /rss feed', function () {
settingsStub.withArgs('public_hash').returns('777aaa');
req.url = req.originalUrl = req.path = '/777aaa/rss';
req.params = {};
res.isPrivateBlog = true;
res.locals = {};
privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
req.url.should.eql('/rss');
});
it('filterPrivateRoutes: allow private rss feed e.g. tags', function () {
settingsStub.withArgs('public_hash').returns('777aaa');
req.url = req.originalUrl = req.path = '/tag/getting-started/777aaa/rss/';
req.params = {};
res.isPrivateBlog = true;
res.locals = {};
privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
req.url.should.eql('/tag/getting-started/rss/');
});
it('[failure] filterPrivateRoutes: allow private rss feed e.g. tags', function () {
settingsStub.withArgs('public_hash').returns('777aaa');
req.url = req.originalUrl = req.path = '/tag/getting-started/rss/';
req.params = {};
res.isPrivateBlog = true;
res.locals = {};
res.redirect = sandbox.spy();
privateBlogging.filterPrivateRoutes(req, res, next);
res.redirect.called.should.be.true();
});
});
});
});

View file

@ -104,6 +104,9 @@
},
"password": {
"defaultValue": ""
},
"public_hash": {
"defaultValue": null
}
}
}

View file

@ -2,6 +2,7 @@ var Settings,
Promise = require('bluebird'),
_ = require('lodash'),
uuid = require('uuid'),
crypto = require('crypto'),
ghostBookshelf = require('./base'),
errors = require('../errors'),
events = require('../events'),
@ -19,7 +20,8 @@ function parseDefaultSettings() {
var defaultSettingsInCategories = require('../data/schema/').defaultSettings,
defaultSettingsFlattened = {},
dynamicDefault = {
db_hash: uuid.v4()
db_hash: uuid.v4(),
public_hash: crypto.randomBytes(15).toString('hex')
};
_.each(defaultSettingsInCategories, function each(settings, categoryName) {