mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-24 09:20:32 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e494a1ea0 |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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]]';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
@@ -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()', () => {
|
||||
|
||||
@@ -869,11 +869,6 @@ describe('Flags', () => {
|
||||
assert.strictEqual(flagData.reports[0].value, '"<script>alert('ok');</script>');
|
||||
});
|
||||
|
||||
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, '"<script>alert('foo');</script>');
|
||||
});
|
||||
|
||||
it('should not allow flagging post in private category', async () => {
|
||||
const category = await Categories.create({ name: 'private category' });
|
||||
|
||||
@@ -1154,7 +1149,5 @@ describe('Flags', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user