wekan/server/lib/tests/attachmentApi.tests.js
Lauri Ojansivu ccd9034339 Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low).
Thanks to Siam Thanat Hack (STH) and xet7 !
2025-11-02 11:42:07 +02:00

203 lines
6.3 KiB
JavaScript

/* eslint-env mocha */
import { expect } from 'chai';
import sinon from 'sinon';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
describe('attachmentApi authentication', function() {
let findOneStub, hashStub;
beforeEach(function() {
hashStub = sinon.stub(Accounts, '_hashLoginToken');
findOneStub = sinon.stub(Meteor.users, 'findOne');
});
afterEach(function() {
if (hashStub) hashStub.restore();
if (findOneStub) findOneStub.restore();
});
// Mock request/response objects
function createMockReq(headers = {}) {
return {
headers,
on: sinon.stub(),
connection: { destroy: sinon.stub() },
};
}
function createMockRes() {
return {
writeHead: sinon.stub(),
end: sinon.stub(),
headersSent: false,
};
}
describe('authenticateApiRequest', function() {
it('denies request with missing X-User-Id header', function() {
const req = createMockReq({ 'x-auth-token': 'sometoken' });
const res = createMockRes();
// Simulate the handler behavior
let errorThrown = false;
try {
if (!req.headers['x-user-id'] || !req.headers['x-auth-token']) {
throw new Meteor.Error('unauthorized', 'Missing X-User-Id or X-Auth-Token headers');
}
} catch (error) {
errorThrown = true;
expect(error.error).to.equal('unauthorized');
}
expect(errorThrown).to.equal(true);
});
it('denies request with missing X-Auth-Token header', function() {
const req = createMockReq({ 'x-user-id': 'user123' });
let errorThrown = false;
try {
if (!req.headers['x-user-id'] || !req.headers['x-auth-token']) {
throw new Meteor.Error('unauthorized', 'Missing X-User-Id or X-Auth-Token headers');
}
} catch (error) {
errorThrown = true;
expect(error.error).to.equal('unauthorized');
}
expect(errorThrown).to.equal(true);
});
it('denies request with invalid token', function() {
const userId = 'user123';
const token = 'invalidtoken';
const req = createMockReq({ 'x-user-id': userId, 'x-auth-token': token });
hashStub.returns('hashedInvalidToken');
findOneStub.returns(null); // No user found
let errorThrown = false;
try {
const hashedToken = Accounts._hashLoginToken(token);
const user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken': hashedToken,
});
if (!user) {
throw new Meteor.Error('unauthorized', 'Invalid credentials');
}
} catch (error) {
errorThrown = true;
expect(error.error).to.equal('unauthorized');
}
expect(errorThrown).to.equal(true);
expect(hashStub.calledOnce).to.equal(true);
expect(findOneStub.calledOnce).to.equal(true);
});
it('allows request with valid X-User-Id and X-Auth-Token', function() {
const userId = 'user123';
const token = 'validtoken';
const req = createMockReq({ 'x-user-id': userId, 'x-auth-token': token });
const hashedToken = 'hashedValidToken';
hashStub.returns(hashedToken);
findOneStub.returns({ _id: userId }); // User found
let authenticatedUserId = null;
try {
const hashed = Accounts._hashLoginToken(token);
const user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken': hashed,
});
if (!user) {
throw new Meteor.Error('unauthorized', 'Invalid credentials');
}
authenticatedUserId = userId;
} catch (error) {
// Should not throw
}
expect(authenticatedUserId).to.equal(userId);
expect(hashStub.calledOnce).to.equal(true);
expect(hashStub.calledWith(token)).to.equal(true);
expect(findOneStub.calledOnce).to.equal(true);
const queryArg = findOneStub.getCall(0).args[0];
expect(queryArg._id).to.equal(userId);
expect(queryArg['services.resume.loginTokens.hashedToken']).to.equal(hashedToken);
});
it('prevents identity spoofing by validating hashed token', function() {
const victimId = 'victim-user-id';
const attackerToken = 'attacker-token';
const req = createMockReq({ 'x-user-id': victimId, 'x-auth-token': attackerToken });
hashStub.returns('hashedAttackerToken');
// Simulate victim exists but token doesn't match
findOneStub.returns(null);
let errorThrown = false;
try {
const hashed = Accounts._hashLoginToken(attackerToken);
const user = Meteor.users.findOne({
_id: victimId,
'services.resume.loginTokens.hashedToken': hashed,
});
if (!user) {
throw new Meteor.Error('unauthorized', 'Invalid credentials');
}
} catch (error) {
errorThrown = true;
expect(error.error).to.equal('unauthorized');
}
expect(errorThrown).to.equal(true);
});
});
describe('request handler DoS prevention', function() {
it('enforces timeout on hanging requests', function(done) {
this.timeout(5000);
const req = createMockReq({ 'x-user-id': 'user1', 'x-auth-token': 'token1' });
const res = createMockRes();
// Simulate timeout behavior
const timeout = setTimeout(() => {
if (!res.headersSent) {
res.headersSent = true;
res.writeHead(408, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'Request timeout' }));
}
}, 100); // Short timeout for test
// Wait for timeout
setTimeout(() => {
expect(res.headersSent).to.equal(true);
expect(res.writeHead.calledWith(408)).to.equal(true);
clearTimeout(timeout);
done();
}, 150);
});
it('limits request body size', function() {
const req = createMockReq({ 'x-user-id': 'user1', 'x-auth-token': 'token1' });
let body = '';
const limit = 50 * 1024 * 1024; // 50MB
// Simulate exceeding limit
body = 'a'.repeat(limit + 1);
expect(body.length).to.be.greaterThan(limit);
// Handler should destroy connection
if (body.length > limit) {
req.connection.destroy();
}
expect(req.connection.destroy.calledOnce).to.equal(true);
});
});
});