Compare commits

..

1 Commits

Author SHA1 Message Date
Misty Release Bot
3e494a1ea0 chore: incrementing version number - v2.8.12 2023-04-26 14:34:44 +00:00
22 changed files with 44 additions and 184 deletions

View File

@@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "2.8.20",
"version": "2.8.12",
"homepage": "http://www.nodebb.org",
"repository": {
"type": "git",
@@ -55,7 +55,7 @@
"cookie-parser": "1.4.6",
"cron": "2.3.0",
"cropperjs": "1.5.13",
"csrf-sync": "4.0.1",
"csurf": "1.11.0",
"daemon": "1.1.0",
"diff": "5.1.0",
"esbuild": "0.16.10",
@@ -90,7 +90,7 @@
"@nodebb/bootswatch": "3.4.2",
"nconf": "0.12.0",
"nodebb-plugin-2factor": "5.1.2",
"nodebb-plugin-composer-default": "9.2.6",
"nodebb-plugin-composer-default": "9.2.5",
"nodebb-plugin-dbsearch": "5.1.5",
"nodebb-plugin-emoji": "4.0.6",
"nodebb-plugin-emoji-android": "3.0.0",
@@ -99,7 +99,7 @@
"nodebb-plugin-spam-be-gone": "1.0.2",
"nodebb-rewards-essentials": "0.2.1",
"nodebb-theme-lavender": "6.0.1",
"nodebb-theme-persona": "12.1.18",
"nodebb-theme-persona": "12.1.12",
"nodebb-theme-slick": "2.0.2",
"nodebb-theme-vanilla": "12.1.19",
"nodebb-widget-essentials": "6.0.1",
@@ -192,4 +192,4 @@
"url": "https://github.com/barisusakli"
}
]
}
}

View File

@@ -374,20 +374,6 @@ get:
type: string
postIndex:
type: number
author:
type: object
required: [username, uid]
properties:
username:
type: string
userslug:
type: string
uid:
type: number
fullname:
type: string
displayname:
type: string
loggedInUser:
$ref: ../../components/schemas/UserObject.yaml#/UserObject
- type: object

View File

@@ -24,7 +24,7 @@ define('forum/topic/images', [], function () {
if (!$this.parent().is('a')) {
$this.wrap('<a href="' + src + '" ' +
(!srcExt && altExt ? ' download="' + utils.escapeHTML(altFilename) + '" ' : '') +
(!srcExt && altExt ? ' download="' + altFilename + '" ' : '') +
' target="_blank" rel="noopener">');
}
});

View File

@@ -15,9 +15,6 @@ app = window.app || {};
reconnectionDelay: config.reconnectionDelay,
transports: config.socketioTransports,
path: config.relative_path + '/socket.io',
query: {
_csrf: config.csrf_token,
},
};
window.socket = io(config.websocketAddress, ioParams);

View File

@@ -237,26 +237,23 @@ Analytics.getDailyStatsForSet = async function (set, day, numDays) {
set = `analytics:${set}`;
}
const daysArr = [];
day = new Date(day);
// set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values
day.setDate(day.getDate() + 1);
day.setHours(0, 0, 0, 0);
async function getHourlyStats(hour) {
while (numDays > 0) {
/* eslint-disable no-await-in-loop */
const dayData = await Analytics.getHourlyStatsForSet(
set,
hour,
day.getTime() - (1000 * 60 * 60 * 24 * (numDays - 1)),
24
);
return dayData.reduce((cur, next) => cur + next);
}
const hours = [];
while (numDays > 0) {
hours.push(day.getTime() - (1000 * 60 * 60 * 24 * (numDays - 1)));
daysArr.push(dayData.reduce((cur, next) => cur + next));
numDays -= 1;
}
return await Promise.all(hours.map(getHourlyStats));
return daysArr;
};
Analytics.getUnwrittenPageviews = function () {

View File

@@ -128,13 +128,12 @@ async function getStats() {
}
let results = await Promise.all([
getStatsFromAnalytics('uniquevisitors', 'uniqueIPCount'),
getStatsForSet('ip:recent', 'uniqueIPCount'),
getStatsFromAnalytics('logins', 'loginCount'),
getStatsForSet('users:joindate', 'userCount'),
getStatsForSet('posts:pid', 'postCount'),
getStatsForSet('topics:tid', 'topicCount'),
]);
results[0].name = '[[admin/dashboard:unique-visitors]]';
results[1].name = '[[admin/dashboard:logins]]';

View File

@@ -1,6 +1,5 @@
'use strict';
const validator = require('validator');
const db = require('../../database');
const events = require('../../events');
const pagination = require('../../pagination');
@@ -40,12 +39,6 @@ eventsController.get = async function (req, res) {
events: eventData,
pagination: pagination.create(page, pageCount, req.query),
types: types,
query: {
start: validator.escape(String(req.query.start)),
end: validator.escape(String(req.query.end)),
username: validator.escape(String(req.query.username)),
group: validator.escape(String(req.query.group)),
perPage: validator.escape(String(req.query.perPage)),
},
query: req.query,
});
};

View File

@@ -119,49 +119,11 @@ uploadsController.uploadCategoryPicture = async function (req, res, next) {
}
if (validateUpload(res, uploadedFile, allowedImageTypes)) {
if (uploadedFile.path.endsWith('.svg')) {
await sanitizeSvg(uploadedFile.path);
}
const filename = `category-${params.cid}${path.extname(uploadedFile.name)}`;
await uploadImage(filename, 'category', uploadedFile, req, res, next);
}
};
async function sanitizeSvg(filePath) {
const dirty = await fs.promises.readFile(filePath, 'utf8');
const clean = sanitizeHtml(dirty, {
allowedTags: [
'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop',
'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect',
'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern',
'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode',
],
allowedAttributes: {
'*': [
// Geometry
'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
'width', 'height', 'd', 'points', 'viewBox', 'transform',
// Presentation
'fill', 'stroke', 'stroke-width', 'opacity',
'stop-color', 'stop-opacity', 'offset', 'style', 'class',
// Text
'text-anchor', 'font-size', 'font-family',
// Misc
'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform',
'xmlns', 'preserveAspectRatio',
],
},
parser: {
lowerCaseTags: false,
lowerCaseAttributeNames: false,
},
});
await fs.promises.writeFile(filePath, clean);
}
uploadsController.uploadFavicon = async function (req, res, next) {
const uploadedFile = req.files.files[0];
const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon'];
@@ -222,6 +184,10 @@ uploadsController.uploadMaskableIcon = async function (req, res, next) {
}
};
uploadsController.uploadLogo = async function (req, res, next) {
await upload('site-logo', req, res, next);
};
uploadsController.uploadFile = async function (req, res, next) {
const uploadedFile = req.files.files[0];
let params;
@@ -242,10 +208,6 @@ uploadsController.uploadFile = async function (req, res, next) {
}
};
uploadsController.uploadLogo = async function (req, res, next) {
await upload('site-logo', req, res, next);
};
uploadsController.uploadDefaultAvatar = async function (req, res, next) {
await upload('avatar-default', req, res, next);
};
@@ -258,9 +220,6 @@ async function upload(name, req, res, next) {
const uploadedFile = req.files.files[0];
if (validateUpload(res, uploadedFile, allowedImageTypes)) {
if (uploadedFile.path.endsWith('.svg')) {
await sanitizeSvg(uploadedFile.path);
}
const filename = name + path.extname(uploadedFile.name);
await uploadImage(filename, 'system', uploadedFile, req, res, next);
}

View File

@@ -9,7 +9,6 @@ const categories = require('../categories');
const plugins = require('../plugins');
const translator = require('../translator');
const languages = require('../languages');
const { generateToken } = require('../middleware/csrf');
const apiController = module.exports;
@@ -65,7 +64,7 @@ apiController.loadConfig = async function (req) {
'cache-buster': meta.config['cache-buster'] || '',
topicPostSort: meta.config.topicPostSort || 'oldest_to_newest',
categoryTopicSort: meta.config.categoryTopicSort || 'newest_to_oldest',
csrf_token: req.uid >= 0 ? generateToken(req) : undefined,
csrf_token: req.uid >= 0 && req.csrfToken && req.csrfToken(),
searchEnabled: plugins.hooks.hasListeners('filter:search.query'),
searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles',
bootswatchSkin: meta.config.bootswatchSkin || '',

View File

@@ -1,7 +1,6 @@
'use strict';
const _ = require('lodash');
const validator = require('validator');
const user = require('../user');
const groups = require('../groups');
@@ -42,9 +41,9 @@ modsController.flags.list = async function (req, res) {
filters = filters.reduce((memo, cur) => {
if (req.query.hasOwnProperty(cur)) {
if (typeof req.query[cur] === 'string' && req.query[cur].trim() !== '') {
memo[cur] = validator.escape(String(req.query[cur].trim()));
memo[cur] = req.query[cur].trim();
} else if (Array.isArray(req.query[cur]) && req.query[cur].length) {
memo[cur] = req.query[cur].map(item => validator.escape(String(item).trim()));
memo[cur] = req.query[cur];
}
}

View File

@@ -105,8 +105,8 @@ topicsController.get = async function getTopic(req, res, next) {
topicData.postIndex = postIndex;
const [author] = await Promise.all([
user.getUserFields(topicData.uid, ['username', 'userslug']),
await Promise.all([
buildBreadcrumbs(topicData),
addOldCategory(topicData, userPrivileges),
addTags(topicData, req, res),
incrementViewCount(req, tid),
@@ -114,7 +114,6 @@ topicsController.get = async function getTopic(req, res, next) {
analytics.increment([`pageviews:byCid:${topicData.category.cid}`]),
]);
topicData.author = author;
topicData.pagination = pagination.create(currentPage, pageCount, req.query);
topicData.pagination.rel.forEach((rel) => {
rel.href = `${url}/topic/${topicData.slug}${rel.href}`;

View File

@@ -629,9 +629,9 @@ SELECT z."value",
ON o."_key" = z."_key"
AND o."type" = z."type"
WHERE o."_key" = $1::TEXT
AND z."value" LIKE $3
AND z."value" LIKE '${match}'
LIMIT $2::INTEGER`,
values: [params.key, params.limit, match],
values: [params.key, params.limit],
});
if (!params.withScores) {
return res.rows.map(r => r.value);

View File

@@ -1,26 +0,0 @@
'use strict';
const { csrfSync } = require('csrf-sync');
const {
generateToken,
csrfSynchronisedProtection,
isRequestValid,
} = csrfSync({
getTokenFromRequest: (req) => {
if (req.headers['x-csrf-token']) {
return req.headers['x-csrf-token'];
} else if (req.body && req.body.csrf_token) {
return req.body.csrf_token;
} else if (req.query) {
return req.query._csrf;
}
},
size: 64,
});
module.exports = {
generateToken,
csrfSynchronisedProtection,
isRequestValid,
};

View File

@@ -2,11 +2,11 @@
const async = require('async');
const path = require('path');
const csrf = require('csurf');
const validator = require('validator');
const nconf = require('nconf');
const toobusy = require('toobusy-js');
const util = require('util');
const { csrfSynchronisedProtection } = require('./csrf');
const plugins = require('../plugins');
const meta = require('../meta');
@@ -34,7 +34,7 @@ middleware.regexes = {
timestampedUpload: /^\d+-.+$/,
};
const csrfMiddleware = csrfSynchronisedProtection;
const csrfMiddleware = csrf();
middleware.applyCSRF = function (req, res, next) {
if (req.uid >= 0) {

View File

@@ -10,7 +10,6 @@ const meta = require('../meta');
const controllers = require('../controllers');
const helpers = require('../controllers/helpers');
const plugins = require('../plugins');
const { generateToken } = require('../middleware/csrf');
let loginStrategies = [];
@@ -109,7 +108,7 @@ Auth.reloadRoutes = async function (params) {
};
if (strategy.checkState !== false) {
req.session.ssoState = generateToken(req, true);
req.session.ssoState = req.csrfToken && req.csrfToken();
opts.state = req.session.ssoState;
}

View File

@@ -34,25 +34,13 @@ Sockets.init = async function (server) {
}
}
io.use(authorize);
io.on('connection', onConnection);
const opts = {
transports: nconf.get('socket.io:transports') || ['polling', 'websocket'],
cookie: false,
allowRequest: (req, callback) => {
authorize(req, (err) => {
if (err) {
return callback(err);
}
const csrf = require('../middleware/csrf');
const isValid = csrf.isRequestValid({
session: req.session || {},
query: req._query,
headers: req.headers,
});
callback(null, isValid);
});
},
};
/*
* Restrict socket.io listener to cookie domain. If none is set, infer based on url.
@@ -74,11 +62,7 @@ Sockets.init = async function (server) {
};
function onConnection(socket) {
socket.uid = socket.request.uid;
socket.ip = (
socket.request.headers['x-forwarded-for'] ||
socket.request.connection.remoteAddress || ''
).split(',')[0];
socket.ip = (socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress || '').split(',')[0];
socket.request.ip = socket.ip;
logger.io_one(socket, socket.uid);
@@ -247,7 +231,9 @@ async function validateSession(socket, errorMsg) {
const cookieParserAsync = util.promisify((req, callback) => cookieParser(req, {}, err => callback(err)));
async function authorize(request, callback) {
async function authorize(socket, callback) {
const { request } = socket;
if (!request) {
return callback(new Error('[[error:not-authorized]]'));
}
@@ -260,13 +246,15 @@ async function authorize(request, callback) {
});
const sessionData = await getSessionAsync(sessionId);
request.session = sessionData;
let uid = 0;
if (sessionData && sessionData.passport && sessionData.passport.user) {
uid = parseInt(sessionData.passport.user, 10);
request.session = sessionData;
socket.uid = parseInt(sessionData.passport.user, 10);
} else {
socket.uid = 0;
}
request.uid = uid;
callback(null, uid);
request.uid = socket.uid;
callback();
}
Sockets.in = function (room) {

View File

@@ -40,10 +40,6 @@ Interstitials.email = async (data) => {
issuePasswordChallenge: !!data.userData.uid && hasPassword,
},
callback: async (userData, formData) => {
if (formData.email) {
formData.email = String(formData.email).trim();
}
// Validate and send email confirmation
if (userData.uid) {
const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10);

View File

@@ -1 +1 @@
data-index="{posts.index}" data-pid="{posts.pid}" data-uid="{posts.uid}" data-timestamp="{posts.timestamp}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" itemprop="comment" itemtype="http://schema.org/Comment" itemscope
data-index="{posts.index}" data-pid="{posts.pid}" data-uid="{posts.uid}" data-timestamp="{posts.timestamp}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" itemscope itemtype="http://schema.org/Comment"

View File

@@ -89,21 +89,6 @@ describe('Sorted Set methods', () => {
assert(data.includes('ddb'));
assert(data.includes('adb'));
});
it('should not error with invalid input', async () => {
const query = `-3217'
OR 1251=CAST((CHR(113)||CHR(98)||CHR(118)||CHR(98)||CHR(113))||(SELECT
(CASE WHEN (1251=1251) THEN 1 ELSE 0
END))::text||(CHR(113)||CHR(113)||CHR(118)||CHR(98)||CHR(113)) AS
NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:read&states[]=watching&states[]=tracking&states[]=notwatching&showLinks=`;
const match = `*${query.toLowerCase()}*`;
const data = await db.getSortedSetScan({
key: 'categories:name',
match: match,
limit: 500,
});
assert.strictEqual(data.length, 0);
});
});
describe('sortedSetAdd()', () => {

View File

@@ -869,11 +869,6 @@ describe('Flags', () => {
assert.strictEqual(flagData.reports[0].value, '&quot;&lt;script&gt;alert(&#x27;ok&#x27;);&lt;&#x2F;script&gt;');
});
it('should escape filters', async () => {
const { body } = await request.get(`${nconf.get('url')}/api/flags?quick="<script>alert('foo');</script>`, { jar });
assert.strictEqual(body.filters.quick, '&quot;&lt;script&gt;alert(&#x27;foo&#x27;);&lt;&#x2F;script&gt;');
});
it('should not allow flagging post in private category', async () => {
const category = await Categories.create({ name: 'private category' });
@@ -1154,7 +1149,5 @@ describe('Flags', () => {
}
});
});
});
});

View File

@@ -95,7 +95,7 @@ helpers.logoutUser = function (jar, callback) {
});
};
helpers.connectSocketIO = function (res, csrf_token, callback) {
helpers.connectSocketIO = function (res, callback) {
const io = require('socket.io-client');
let cookies = res.headers['set-cookie'];
cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c));
@@ -106,9 +106,6 @@ helpers.connectSocketIO = function (res, csrf_token, callback) {
Origin: nconf.get('url'),
Cookie: cookie,
},
query: {
_csrf: csrf_token,
},
});
socket.on('connect', () => {

View File

@@ -73,7 +73,7 @@ describe('socket.io', () => {
}, (err, res) => {
assert.ifError(err);
helpers.connectSocketIO(res, body.csrf_token, (err, _io) => {
helpers.connectSocketIO(res, (err, _io) => {
io = _io;
assert.ifError(err);