Skip to content

Commit fbe9b77

Browse files
committed
fix(wac): require Control on protected resource for POST-created .acl/.meta sidecars
A POST whose Slug resolves to an `.acl`/`.meta` sidecar is currently authorized only against the *container* (the request path), because the dedicated ACL Control guard in `auth/middleware.js` (`authorizeAclAccess`) keys on `urlPath.endsWith('.acl')` — which never matches a container POST. The sidecar filename is only produced *inside* `handlePost` via `generateUniqueFilename`, after authorization has run. Impact: an agent holding only `acl:Append` on a container (e.g. a public-append inbox/upload directory created by `generateInboxAcl`) can `POST` with `Slug: victim.acl` and write a resource ACL that grants itself `acl:Control`/`acl:Read` on a sibling — privilege escalation to full control of a resource it had no access to. The slug validator permits `.`, so `victim.acl` passes. Fix: in `handlePost`, when the resolved child filename ends in `.acl`/`.meta`, require `acl:Control` on the protected resource (the sidecar path minus the suffix) before writing, mirroring `authorizeAclAccess`. Owners (who hold Control) are unaffected; Append-only agents get 403. Found during a cross-implementation audit against the solid-pod-rs Rust port, which shared the same gap and is fixed in lockstep. Reproduction and a proposed regression test are in the PR description; the full integration harness could not be exercised in the contributor's environment (missing optional `@simplewebauthn/server` dep used by the passkey path at server bootstrap), so CI validation is requested. Co-Authored-By: jjohare <github@thedreamlab.uk>
1 parent 10bd60f commit fbe9b77

1 file changed

Lines changed: 29 additions & 1 deletion

File tree

src/handlers/container.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { initializeQuota, checkQuota, updateQuotaUsage } from '../storage/quota.
33
import { getAllHeaders } from '../ldp/headers.js';
44
import { isContainer, getEffectiveUrlPath, getPodName } from '../utils/url.js';
55
import { generateProfile, generatePreferences, generateTypeIndex, serialize } from '../webid/profile.js';
6-
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, generatePublicFolderAcl, serializeAcl, relativizeOwnerWebId } from '../wac/parser.js';
6+
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, generatePublicFolderAcl, serializeAcl, relativizeOwnerWebId, AccessMode } from '../wac/parser.js';
7+
import { checkAccess } from '../wac/checker.js';
78
import { provisionOwnerKey, assertProvisionKeysCompatible } from '../keys/provision.js';
89
import { createToken } from '../auth/token.js';
910
import { canAcceptInput, toJsonLd, RDF_TYPES } from '../rdf/conneg.js';
@@ -88,6 +89,33 @@ export async function handlePost(request, reply) {
8889
const newStoragePath = storagePath + filename + (isCreatingContainer ? '/' : '');
8990
const resourceUrl = `${request.protocol}://${request.hostname}${newUrlPath}`;
9091

92+
// Security: a Slug that resolves to an `.acl`/`.meta` sidecar governs
93+
// ANOTHER resource's permissions. The authorize() preHandler only checked
94+
// Append/Write on the *container* (the request path), and its dedicated
95+
// `.acl` Control guard (authorizeAclAccess) never fires here because the
96+
// request path is the container, not the resolved sidecar. Without this an
97+
// agent with mere Append rights on a container could POST `Slug: victim.acl`
98+
// and self-grant Control on a sibling resource — privilege escalation.
99+
// Mirror authorizeAclAccess: require acl:Control on the protected resource
100+
// before minting a sidecar via POST.
101+
if (!isCreatingContainer && /\.(acl|meta)$/.test(filename)) {
102+
const protectedUrlPath = newUrlPath.replace(/\.(acl|meta)$/, '');
103+
const protectedStoragePath = newStoragePath.replace(/\.(acl|meta)$/, '');
104+
const { allowed } = await checkAccess({
105+
resourceUrl: `${request.protocol}://${request.hostname}${protectedUrlPath}`,
106+
resourcePath: protectedStoragePath,
107+
isContainer: protectedUrlPath.endsWith('/'),
108+
agentWebId: request.webId,
109+
requiredMode: AccessMode.CONTROL
110+
});
111+
if (!allowed) {
112+
return reply.code(403).send({
113+
error: 'Forbidden',
114+
message: 'Creating an ACL/meta sidecar via POST requires Control on the protected resource'
115+
});
116+
}
117+
}
118+
91119
let success;
92120
if (isCreatingContainer) {
93121
success = await storage.createContainer(newStoragePath);

0 commit comments

Comments
 (0)