mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 10:13:11 +02:00
fix: close low-trust self detail bypass
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0a2a16a26d
commit
da1778fbf8
7 files changed
+59
-36
No files matched your search
@@ -615,6 +615,20 @@ describeEmbeddedPostgres("low-trust red-team HTTP route regression suite", () =>
|
||||
expect(lowTrustRes.body).not.toHaveProperty("access");
|
||||
expectNoCanary(lowTrustRes.body, fixture.canaries.agentConfig);
|
||||
|
||||
const lowTrustSelfByIdRes = await request(createApp(db, agentActor(fixture)))
|
||||
.get(`/api/agents/${fixture.agents.lowTrust.id}`);
|
||||
expect(lowTrustSelfByIdRes.status, JSON.stringify(lowTrustSelfByIdRes.body)).toBe(200);
|
||||
expect(lowTrustSelfByIdRes.body).toMatchObject({
|
||||
id: fixture.agents.lowTrust.id,
|
||||
companyId: fixture.company.id,
|
||||
trustPreset: LOW_TRUST_REVIEW_PRESET,
|
||||
});
|
||||
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("adapterConfig");
|
||||
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("runtimeConfig");
|
||||
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("permissions");
|
||||
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("access");
|
||||
expectNoCanary(lowTrustSelfByIdRes.body, fixture.canaries.agentConfig);
|
||||
|
||||
const standardActor = agentActor(fixture, fixture.agents.standard.id);
|
||||
const standardRes = await request(createApp(db, { ...standardActor, runId: null })).get("/api/agents/me");
|
||||
expect(standardRes.status, JSON.stringify(standardRes.body)).toBe(200);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function readObject(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
@@ -101,6 +101,7 @@ import { getTelemetryClient } from "../telemetry.js";
|
||||
import { assertEnvironmentSelectionForCompany } from "./environment-selection.js";
|
||||
import { recoveryService } from "../services/recovery/service.js";
|
||||
import { resolveCoreTrustPreset } from "../services/trust-preset-resolver.js";
|
||||
import { readObject } from "../lib/objects.js";
|
||||
|
||||
const RUN_LOG_DEFAULT_LIMIT_BYTES = 256_000;
|
||||
const RUN_LOG_MAX_LIMIT_BYTES = 1024 * 1024;
|
||||
@@ -118,12 +119,6 @@ function readLiveRunsQueryInt(value: unknown, max: number, fallback = 0) {
|
||||
return Math.min(max, Math.trunc(parsed));
|
||||
}
|
||||
|
||||
function readObject(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function readRunIssueId(context: Record<string, unknown> | null) {
|
||||
const directIssueId = context?.issueId;
|
||||
if (typeof directIssueId === "string" && isUuidLike(directIssueId)) return directIssueId;
|
||||
@@ -1915,6 +1910,17 @@ export function agentRoutes(
|
||||
assertCompanyAccess(req, agent.companyId);
|
||||
if (!(await assertAgentReadAllowed(req, res, agent))) return;
|
||||
const isSelf = req.actor.type === "agent" && req.actor.agentId === id;
|
||||
if (isSelf) {
|
||||
const trustPreset = await resolveAgentSelfTrustPreset(req, agent);
|
||||
if (trustPreset.kind === "denied") {
|
||||
res.status(403).json({ error: trustPreset.detail });
|
||||
return;
|
||||
}
|
||||
if (trustPreset.kind === "low_trust_review") {
|
||||
res.json(buildLowTrustSelfView(agent));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const canReadSensitiveDetail = isSelf
|
||||
? true
|
||||
: await actorCanReadConfigurationsForCompany(req, agent.companyId);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { assertCompanyAccess } from "./authz.js";
|
||||
import { parseProjectExecutionWorkspacePolicy } from "../services/execution-workspace-policy.js";
|
||||
import { isLowTrustRuntimeManagementAllowed } from "../services/low-trust-runtime-containment.js";
|
||||
import { resolveCoreTrustPreset, type TrustPresetResolution } from "../services/trust-preset-resolver.js";
|
||||
import { readObject } from "../lib/objects.js";
|
||||
|
||||
const WORKSPACE_RUNTIME_ELIGIBLE_ISSUE_STATUSES: string[] = [
|
||||
"backlog",
|
||||
@@ -16,12 +17,6 @@ const WORKSPACE_RUNTIME_ELIGIBLE_ISSUE_STATUSES: string[] = [
|
||||
"blocked",
|
||||
];
|
||||
|
||||
function readObject(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function readRunIssueId(context: Record<string, unknown> | null) {
|
||||
const directIssueId = context?.issueId;
|
||||
if (typeof directIssueId === "string" && directIssueId.length > 0) return directIssueId;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
agents,
|
||||
@@ -618,19 +618,27 @@ export function authorizationService(db: Db) {
|
||||
}
|
||||
|
||||
async function issueIdIsDescendantOf(issueId: string, rootIssueId: string, companyId: string) {
|
||||
let cursor: string | null = issueId;
|
||||
// Keep list filtering bounded. Deep task trees outside this limit are treated as outside the boundary.
|
||||
for (let depth = 0; cursor && depth < LOW_TRUST_ISSUE_ANCESTRY_MAX_DEPTH; depth += 1) {
|
||||
if (cursor === rootIssueId) return true;
|
||||
const row: { id: string; companyId: string; parentId: string | null } | null = await db
|
||||
.select({ id: issues.id, companyId: issues.companyId, parentId: issues.parentId })
|
||||
.from(issues)
|
||||
.where(eq(issues.id, cursor))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
if (!row || row.companyId !== companyId) return false;
|
||||
cursor = row.parentId;
|
||||
}
|
||||
return false;
|
||||
const rows = await db.execute(sql`
|
||||
WITH RECURSIVE ancestors(id, parent_id, depth) AS (
|
||||
SELECT id, parent_id, 0
|
||||
FROM issues
|
||||
WHERE company_id = ${companyId}
|
||||
AND id = ${issueId}
|
||||
UNION ALL
|
||||
SELECT parent.id, parent.parent_id, ancestors.depth + 1
|
||||
FROM issues parent
|
||||
JOIN ancestors ON parent.id = ancestors.parent_id
|
||||
WHERE parent.company_id = ${companyId}
|
||||
AND ancestors.depth < ${LOW_TRUST_ISSUE_ANCESTRY_MAX_DEPTH - 1}
|
||||
)
|
||||
SELECT EXISTS(SELECT 1 FROM ancestors WHERE id = ${rootIssueId}) AS is_descendant
|
||||
`);
|
||||
const first = Array.isArray(rows) ? rows[0] : null;
|
||||
return Boolean(
|
||||
first &&
|
||||
typeof first === "object" &&
|
||||
(first as Record<string, unknown>).is_descendant === true,
|
||||
);
|
||||
}
|
||||
|
||||
async function issueResourceWithinLowTrustBoundary(
|
||||
|
||||
@@ -8100,15 +8100,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
)
|
||||
: [];
|
||||
})();
|
||||
assertLowTrustRuntimeServicesAllowed({
|
||||
resolution: trustPreset,
|
||||
runtimeServiceCount: runtimeServiceIntents.length,
|
||||
});
|
||||
if (runtimeServiceIntents.length > 0) {
|
||||
context.paperclipRuntimeServiceIntents = runtimeServiceIntents;
|
||||
} else {
|
||||
delete context.paperclipRuntimeServiceIntents;
|
||||
}
|
||||
assertLowTrustRuntimeServicesAllowed({
|
||||
resolution: trustPreset,
|
||||
runtimeServiceCount: runtimeServiceIntents.length,
|
||||
});
|
||||
if (executionWorkspace.projectId && !readNonEmptyString(context.projectId)) {
|
||||
context.projectId = executionWorkspace.projectId;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type SourceTrustMetadata,
|
||||
} from "@paperclipai/shared";
|
||||
import { forbidden } from "../errors.js";
|
||||
import { readObject } from "../lib/objects.js";
|
||||
import { resolveCoreTrustPreset } from "./trust-preset-resolver.js";
|
||||
|
||||
export const LOW_TRUST_QUARANTINED_BODY =
|
||||
@@ -70,12 +71,6 @@ export function buildLowTrustSourceTrust(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function readObject(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
export function buildPromotedSourceTrust(input: {
|
||||
sourceIssueId: string;
|
||||
sourceArtifactKind: "comment" | "document" | "work_product" | "issue";
|
||||
|
||||
Reference in new issue
Block a user