mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-22 14:07:28 +00:00
Check if user is suspended on WebDAV basic auth and others (#3369)
This commit is contained in:
@@ -335,6 +335,63 @@ describe('WebDAVController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('suspension gate', () => {
|
||||
// WebDAV must also enforce the suspension gate every other authenticated
|
||||
// route gets from requireAuthGate. Same single-use() dispatch means that
|
||||
// middleware is never wired in, so #dispatch calls assertNotSuspended
|
||||
// itself. Without it, a suspended account could read/write/delete its
|
||||
// whole filesystem over the `dav` subdomain.
|
||||
it('rejects a suspended session actor with 403', async () => {
|
||||
const { res, captured } = makeRes();
|
||||
await dispatchMiddleware(
|
||||
makeReq({
|
||||
method: 'PROPFIND',
|
||||
actor: {
|
||||
user: {
|
||||
id: 1,
|
||||
uuid: 'suspended-uuid',
|
||||
username: 'suspended',
|
||||
suspended: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
res,
|
||||
noop,
|
||||
);
|
||||
expect(captured.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('rejects a suspended user on the Basic-auth path with 403', async () => {
|
||||
const username = `webdav-suspended-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 10)}`;
|
||||
const created = await server.stores.user.create({
|
||||
username,
|
||||
uuid: uuidv4(),
|
||||
password: await bcryptHash('correct-horse', 4),
|
||||
email: `${username}@test.local`,
|
||||
free_storage: 100 * 1024 * 1024,
|
||||
requires_email_confirmation: false,
|
||||
});
|
||||
await server.stores.user.update(created.id, {
|
||||
suspended: 1,
|
||||
});
|
||||
|
||||
const { res, captured } = makeRes();
|
||||
await dispatchMiddleware(
|
||||
makeReq({
|
||||
method: 'PROPFIND',
|
||||
headers: {
|
||||
authorization: basicAuth(username, 'correct-horse'),
|
||||
},
|
||||
}),
|
||||
res,
|
||||
noop,
|
||||
);
|
||||
expect(captured.statusCode).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsupported methods', () => {
|
||||
it('returns 405 for unknown HTTP methods', async () => {
|
||||
const { res, captured } = makeRes();
|
||||
|
||||
@@ -23,7 +23,10 @@ import { posix as pathPosix } from 'node:path';
|
||||
import { EventMap } from '../../clients/event/types.js';
|
||||
import type { Actor } from '../../core/actor.js';
|
||||
import { HttpError } from '../../core/http/HttpError.js';
|
||||
import { assertVerifiedAccount } from '../../core/http/middleware/gates.js';
|
||||
import {
|
||||
assertNotSuspended,
|
||||
assertVerifiedAccount,
|
||||
} from '../../core/http/middleware/gates.js';
|
||||
import type { PuterRouter } from '../../core/http/PuterRouter.js';
|
||||
import { verify as verifyOtp } from '../../services/auth/OTPUtil.js';
|
||||
import { expandTildePath } from '../../services/fs/resolveNode.js';
|
||||
@@ -90,14 +93,15 @@ export class WebDAVController extends PuterController {
|
||||
const actor = await this.#resolveActor(req, res);
|
||||
if (!actor) return; // 401 already sent
|
||||
|
||||
// Apply the same pending-verification gate every other authenticated
|
||||
// route gets from `requireVerifiedAccount`. WebDAV dispatches every
|
||||
// method off a single `router.use` with no route options, so that
|
||||
// middleware is never inserted into its chain — without this call an
|
||||
// account still pending email / phone / card verification could read,
|
||||
// write, and delete its entire filesystem over the `dav` subdomain,
|
||||
// bypassing the gate. Throws a 403 HttpError, surfaced by the catch in
|
||||
// registerRoutes.
|
||||
// Apply the same suspension + pending-verification gates every other
|
||||
// authenticated route gets from `requireAuthGate` / `requireVerifiedAccount`.
|
||||
// WebDAV dispatches every method off a single `router.use` with no route
|
||||
// options, so that middleware is never inserted into its chain — without
|
||||
// these calls a suspended account (or one still pending email / phone /
|
||||
// card verification) could read, write, and delete its entire filesystem
|
||||
// over the `dav` subdomain, bypassing the gates. Both throw a 403
|
||||
// HttpError, surfaced by the catch in registerRoutes.
|
||||
assertNotSuspended(actor.user);
|
||||
assertVerifiedAccount(actor.user);
|
||||
|
||||
// Expand `~`/`~/...` against the authenticated actor's username.
|
||||
|
||||
@@ -93,12 +93,10 @@ export const requireAuthGate = (): RequestHandler => {
|
||||
next(rejectAuth(req));
|
||||
return;
|
||||
}
|
||||
if (req.actor.user.suspended) {
|
||||
next(
|
||||
new HttpError(403, 'Account suspended', {
|
||||
legacyCode: 'forbidden',
|
||||
}),
|
||||
);
|
||||
try {
|
||||
assertNotSuspended(req.actor.user);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
@@ -312,6 +310,16 @@ export const assertVerifiedAccount = (
|
||||
}
|
||||
};
|
||||
|
||||
export const assertNotSuspended = (
|
||||
user: { suspended?: unknown } | undefined,
|
||||
): void => {
|
||||
if (user?.suspended) {
|
||||
throw new HttpError(403, 'Account suspended', {
|
||||
legacyCode: 'forbidden',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reject unless the actor is acting through one of the named apps.
|
||||
* App-under-user actors are permitted iff `actor.app.uid` is in the allowList;
|
||||
|
||||
Reference in New Issue
Block a user