Skip to content

Commit 58c553a

Browse files
committed
fix(interfaces): bound descriptions in the service and rate-limit OTP verify
Two gaps recovered from the copilot-tool and duplicate-implementation audits. - `MAX_INTERFACE_DESCRIPTION_LENGTH` was enforced only in the HTTP contract, but the contract is not the only writer: the copilot's `user_interface` tool calls `createInterface`/`updateInterfaceDescription` directly, so the cap did not apply to it. An unbounded description is then re-serialized into every subsequent get/list response and into the copilot's VFS metadata each turn — recurring context cost, not just row bloat. Now asserted beside the name check that was already there for the same reason. - The public file OTP route rate-limited the send path but not verify, while the interfaces route limits both. Added the matching per-IP bucket, kept separate from the send bucket so failed verifies cannot throttle a legitimate resend.
1 parent e2f389e commit 58c553a

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

apps/sim/app/api/files/public/[token]/otp/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ export const PUT = withRouteHandler(
131131
const requestId = generateRequestId()
132132

133133
try {
134+
// Its own IP bucket, separate from the send path above, so failed verifies
135+
// never throttle a legitimate resend.
136+
const ip = getClientIp(request)
137+
const ipRateLimit = await rateLimiter.checkRateLimitDirect(
138+
`file-otp:verify:ip:${ip}`,
139+
OTP_IP_RATE_LIMIT
140+
)
141+
if (!ipRateLimit.allowed) {
142+
logger.warn(`[${requestId}] OTP verify IP rate limit exceeded from ${ip}`)
143+
return rateLimited(ipRateLimit.retryAfterMs, OTP_IP_RATE_LIMIT.refillIntervalMs)
144+
}
145+
134146
const parsed = await parseRequest(verifyPublicFileOtpContract, request, context)
135147
if (!parsed.success) return parsed.response
136148
const { token } = parsed.data.params

apps/sim/lib/interfaces/service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { generateRestoreName, restoreWithUniqueName } from '@/lib/core/utils/res
2828
import {
2929
createEmptyLayout,
3030
createInterfaceModule,
31+
MAX_INTERFACE_DESCRIPTION_LENGTH,
3132
MAX_INTERFACE_NAME_LENGTH,
3233
} from '@/lib/interfaces/constants'
3334
import { moveModuleToCell, overlappingModules } from '@/lib/interfaces/geometry'
@@ -153,6 +154,25 @@ function assertValidName(name: string): void {
153154
}
154155
}
155156

157+
/**
158+
* Bounds a description here rather than only at the HTTP contract.
159+
*
160+
* The contract is not the only writer: the copilot's `user_interface` tool calls
161+
* this service directly, so a cap that lives solely in the boundary schema is a
162+
* cap the tool does not have. An unbounded description is persisted, then
163+
* re-serialized into every subsequent `get`/`list` response and into the
164+
* copilot's VFS metadata on every turn — so it is recurring context cost, not
165+
* just row bloat. Mirrors {@link assertValidName}, which was already here for
166+
* exactly this reason.
167+
*/
168+
function assertValidDescription(description: string | null): void {
169+
if (description !== null && description.length > MAX_INTERFACE_DESCRIPTION_LENGTH) {
170+
throw new Error(
171+
`Interface description exceeds maximum length (${MAX_INTERFACE_DESCRIPTION_LENGTH} characters)`
172+
)
173+
}
174+
}
175+
156176
/**
157177
* Lists interfaces in a workspace, ordered by creation time.
158178
*/
@@ -210,6 +230,7 @@ export async function getInterfaceById(
210230
*/
211231
export async function createInterface(data: CreateInterfaceData): Promise<InterfaceDefinition> {
212232
assertValidName(data.name)
233+
assertValidDescription(data.description ?? null)
213234

214235
const now = new Date()
215236
try {
@@ -276,6 +297,8 @@ export async function updateInterfaceDescription(
276297
id: string,
277298
description: string | null
278299
): Promise<InterfaceDefinition> {
300+
assertValidDescription(description)
301+
279302
const [row] = await db
280303
.update(workspaceInterface)
281304
.set({ description, updatedAt: new Date() })

0 commit comments

Comments
 (0)