Methodology
BuilderProof Editorial Team17 min read26 views

File-Upload and Media-Handling Posture: A Proposed Axis for What an AI Builder's Upload Path Actually Permits (August 2026)

A proposed BuilderProof benchmark axis measuring whether the file-upload path an AI app builder emits is owner-scoped, content-verified, bounded and durable, or whether it only works because the developer testing it is the only account in the app. Six weighted signals, four posture levels, a reproducible protocol.

Minimalist blueprint illustration of a sealed parcel on a guide line whose external tag alone is read by a scanning beam, while a different shape ghosted inside the parcel goes uninspected, and three identical open containers with identical latches wait beyond the gate
Minimalist blueprint illustration of a sealed parcel on a guide line whose external tag alone is read by a scanning beam, while a different shape ghosted inside the parcel goes uninspected, and three identical open containers with identical latches wait beyond the gate
On this page

Quick answer (August 2026). File-upload and media-handling posture measures whether the upload path an AI app builder generates is safe, bounded and durable, or whether it only works because the person testing it is the only person using it. It is a proposed BuilderProof axis, drafted August 23, 2026, scoring six weighted signals read from the emitted code and the emitted storage policy: object-level authorization, server-side content verification, size-ceiling handling, object-key generation, serving posture, and lifecycle coupling. This page is a pre-registration. No builder has been scored against it, and no placements are published here.

Every builder in the cohort will wire up an avatar picker in one prompt. The file lands, the thumbnail appears, the demo works. The interesting question is not whether the upload succeeds. It is what the emitted code decided on your behalf about who may read that object afterwards, what it believed about the file's type, and what happens when the file is larger than the one you tested with.

Why we are proposing this axis

The usual story about AI builders and file storage is that they leave buckets wide open. As of August 2026 that story is out of date at the platform layer, and the evidence is in the vendors' own documentation.

Supabase

Supabase, the storage backend underneath several builders in this cohort, states the default plainly: "By default Storage does not allow any uploads to buckets without RLS policies." That is fail-closed. Nothing writes until somebody writes a policy (Supabase, Storage Access Control, 2026).

Lovable

Lovable goes further in its own documentation, describing private as "(the default): files are only accessible by users with the correct permissions, controlled by row level security policies" and stating that "Public storage buckets are blocked by default on all plans. This prevents users from creating publicly accessible storage buckets." It also recommends the safer serving path directly: "For most apps, keeping buckets private and letting your app serve files through signed URLs is the safer choice" (Lovable, Storage docs, 2026).

So the platform defaults are good, and that is precisely the reason this axis is worth building. When the default is fail-closed, the app does not work until something unblocks it, and the thing that unblocks it is code and policy the builder writes for you under pressure to make the preview stop erroring. The risk did not disappear. It moved one layer down, from the bucket's public flag into the shape of the predicate the model wrote to get past the flag. That is a question about emitted artifacts, which is the only kind of question this lab is set up to answer.

Searching for prior work returns two disconnected literatures. One is general secure-upload guidance written for human engineers. The other is AI-builder comparison content scoring speed, price and looks. We found no reproducible, tool-by-tool measurement of the upload path these tools actually emit. That gap is what this axis is built to close.

What "file-upload and media-handling posture" means here

The scope is narrow on purpose, and three fences keep it honest.

It is read from the untouched output, at the moment of generation. Not after a human hardens it, and not from a vendor's security marketing page. If the emitted policy is permissive, that is the posture, regardless of what the docs say is possible.

It covers binary objects and their access path, not rows. The trust boundary here is different in kind from the one our validation work covers. A JSON field arrives as text and is checked against a schema. A file arrives as opaque bytes accompanied by a set of claims the client makes about those bytes, and it comes to rest in a second service with its own separate authorization model. Those are different failure modes and they need different signals.

A decisive part of the evidence may not be in the repository. On several builders the storage policy lives in the backend project, not in the exported code. An axis that only reads the repo would silently score that signal as absent. We treat an out-of-repo policy as in scope and record where it was read from, because scoring absence of evidence as absence of the thing is how a rubric quietly rewards opacity.

The proposed rubric

Six signals, each anchored to published guidance, each independently checkable. The weights are a starting point for community revision, not a settled formula.

Scroll to see more

SignalWeightWhat a failing case looks like
Object-level authorization25The emitted storage policy admits any authenticated user, so every account can read and overwrite every other account's objects
Server-side content verification20The only check on file type is the client-supplied Content-Type header or the filename extension, with no signature check on the bytes
Size-ceiling handling15No explicit limit anywhere on the emitted path, and the platform's own ceiling surfaces to the user as an unhandled error or a silent hang
Object-key generation15The key is built from the user-supplied filename, unsanitised, so the caller influences where the object lands
Serving posture15Objects are served from a permanent unguessable-by-hope public URL rather than a short-lived signed one, with no expiry
Lifecycle coupling10Deleting the owning record leaves the object behind forever, with no emitted cleanup and no documented retention

Object-level authorization carries the most weight because it is the only signal on the list whose failure is unbounded. A missing size limit costs money. A permissive read policy exposes every file every user has ever uploaded, and it does so quietly.

The four posture levels

For legibility, the weighted score maps to one of four levels.

Level 0, open shelf. Uploads work and objects are reachable by anyone with the URL. Either the bucket is public or no meaningful policy was emitted.

Level 1, logged-in shelf. A policy exists and it is fail-closed against anonymous callers. Its predicate is membership rather than ownership, so the app is protected from strangers and not from its own users.

Level 2, owner-scoped. Objects are scoped to the account that uploaded them, the bucket is private, and reads go through short-lived signed URLs.

Level 3, verified and durable. Level 2 plus server-side verification of the bytes, generated object keys, an explicit and handled size ceiling on the emitted path, and a deletion path coupled to the owning record.

Authenticated is not authorized

This is the failure the axis is really built around, and it is worth naming precisely because it is so easy to ship.

Supabase's documented pattern for per-user isolation checks the object's path against the caller's identity, using a policy of the form (storage.foldername(name))[1] = (select auth.jwt()->>'sub'), described as allowing "authenticated users to upload files to a folder called with their users.id". There is also an individual-access pattern keyed on the object's owner_id. Both express ownership.

The shorter policy that also makes the error go away is one that requires only that the caller is signed in. It is fail-closed against anonymous traffic, it satisfies the platform's fail-closed default, and it makes the upload work on the first try. It also grants every account in the application read and write access to every object in the bucket.

The reason this survives testing is structural rather than careless. During development there is exactly one account, and it is yours. A policy that says this user and a policy that says any user return an identical result for every request you will make, because in a single-account app those two sets are the same set. There is no flaky behaviour to notice and no error to chase. The defect becomes observable at the moment a second person signs up, which is typically after launch.

We have named neighbouring versions of this shape on other axes: a defect that is invisible in a single-user preview, and a sending path whose tested population and failing population do not overlap. This is a third mechanism, distinct from both. It is not concurrency and it is not an allowlist. It is a predicate that is correct in form, checks the wrong thing, and is indistinguishable from the correct predicate under the only test conditions the builder's own workflow provides.

The label on the outside of the box

The second signal is about believing the wrong witness.

OWASP is unambiguous about the header: "The Content-Type for uploaded files is provided by the user, and as such cannot be trusted, as it is trivial to spoof." Its remedy is to inspect the bytes, since "Validating the file's signature can be checked and verified against the expected file that should be received." On extensions it is equally direct, calling for an allow list because "Blocking specific extensions is a weak protection method on its own" (OWASP, File Upload Cheat Sheet, 2026).

v0 by Vercel

Now compose that with the storage layer. Supabase lets a bucket be restricted at creation time by "providing allowedMimeTypes and maxFileSize", so that you can "allow your users to upload only images to the avatars bucket", and "If an upload request doesn't meet the above restrictions it will be rejected" (Supabase, Creating buckets, 2026).

Here is the analytical point, and we have not seen it stated for this cohort: a bucket-level MIME allowlist is an allowlist over an attacker-controlled field. The value it filters is the same client-supplied Content-Type that OWASP says cannot be trusted. Configuring allowedMimeTypes: ['image/png'] is therefore an excellent guard against a user accidentally selecting the wrong file, and it is not a guard against a user who intends to send something else, because the sender chooses the string that the filter reads.

That does not make the setting useless. It makes it a different kind of control than it appears to be, and it means a builder that configures it has earned a real usability point and not a security one. Full marks on this signal require the emitted server-side code to check the file's signature against its claimed type. We score the configuration and the byte check separately, and we say which one earned the points.

Two ceilings, on two different layers

The third signal exists because size failures are architectural, not incidental, and which ceiling an app hits is decided by a choice the builder makes silently.

If the emitted app proxies the upload through its own server route, the binding constraint belongs to the hosting platform. Vercel documents it exactly: "The maximum payload size for the request body or the response body of a Vercel Function is 4.5 MB. If a Vercel Function receives a payload in excess of the limit it will return an error 413: FUNCTION_PAYLOAD_TOO_LARGE" (Vercel, Functions Limits, 2026).

Bolt.new

If the emitted app uploads from the browser straight to storage, the constraint belongs to the storage client instead. Supabase describes its standard upload method as "ideal for small files that are not larger than 6MB" and recommends "using TUS Resumable Upload for uploading files greater than 6MB in size for better reliability" (Supabase, Standard Uploads, 2026).

Two things follow. First, both ceilings sit below the size of an ordinary photograph taken on a current phone, so the failing file is not an adversarial payload, it is a holiday picture. Second, and this is the part a rubric can actually measure, the two numbers are attached to different layers and neither is visible in the generated interface. A file picker with no accept attribute, no client-side size check and no error branch looks identical whichever architecture sits behind it. The axis therefore scores the emitted path shape and the presence of a handled failure branch, not the raw number, because the number is a property of the host and the shape is a property of the output.

Supabase also documents the collision behaviour on repeat uploads, where "the default behavior is to return a 400 Asset Already Exists error" unless upsert is set. A generated retry loop that does not handle that status is a small, cheap, testable defect, and it is the sort of thing a fixed protocol catches.

The object key is part of the authorization decision

The fourth signal usually gets filed under tidiness. On this stack it is not tidiness.

Recall the documented ownership pattern: the policy reads the first path segment of the object's key and compares it to the caller's identity. That means the key is the authorization predicate's input. Any part of the key the caller can influence is a part of the access-control decision the caller can influence.

OWASP's guidance lands squarely on top of that: "Creating a random string as a filename, such as generating a UUID/GUID, is essential", and "Restrict the use of leading periods (hidden files) and sequential periods (directory traversal)." On a plain filesystem those rules prevent a file escaping its directory. On a path-scoped storage policy they also prevent a caller from writing itself into somebody else's namespace. A builder that emits ${user.id}/${file.name} has done most of the work. A builder that emits ${file.name} has moved the boundary into user-controlled space without any code that looks wrong.

Serving, and the cost that never shows up in the demo

The fifth and sixth signals cover what happens after the byte is at rest.

A permanent public object URL is not an access control, it is an absence of one, and it is permanent in a way that a revoked account is not. Lovable's own recommendation, quoted above, is signed URLs from a private bucket, and its documentation notes that for private buckets the dashboard "copies a temporary signed URL that expires after one hour." Expiry is the property being scored here, not the presence of a URL.

Lifecycle coupling carries the smallest weight because its consequence is money rather than exposure, but it is the signal most reliably absent. Delete the row and the object usually stays. Supabase's own scaling guidance points at where that bill lands, advising that images "typically make up most of your egress" and that caching helps because "we charge less for cached egress" (Supabase, Storage scaling, 2026). Storage is the one part of a generated app whose default behaviour is to accumulate, forever, with no prompt from anyone.

How to reproduce it

The protocol is deliberately boring. That is the point.

  1. Generate a fixed reference app from the standard prompt suite, one generation per builder, no manual edits. The reference prompt asks for a feature that requires a user-owned file, described as a product feature and never in terms of its defenses.
  2. Record the emitted upload path verbatim: the client call, any server route it passes through, and the storage client configuration.
  3. Retrieve the storage policy as generated, from wherever it lives, and record its full text plus its source. If no policy was emitted, record that, and record whether uploads still succeed.
  4. Create a second account and attempt to read and overwrite the first account's object. This is the step that separates level 1 from level 2, it is the only step that requires a second identity, and it is the step a builder's own workflow will never perform. It is the most important instruction in this protocol.
  5. Send a fixed battery: a file whose bytes contradict its declared Content-Type, a file whose extension contradicts both, a file above each documented ceiling, a filename containing traversal sequences and a leading period, and a repeat upload to an existing key. Record the status and the response for each.
  6. Delete the owning record and check whether the object is still retrievable.
  7. Publish the full request and response log next to the score, so the arithmetic is auditable, in line with our reproducibility-first methodology.

Fairness guardrail. A tool that emits front-end components only has no generated storage path at all. That is recorded as out of scope for the server-presupposing signals, not scored as zero. Scoring absence as failure is how a rubric quietly punishes a product for being a different kind of product.

Why we are naming a pattern and not publishing placements

In August 2026 this lab withdrew its June output-quality result set and the composite that depended on it, because the underlying runs could not be reproduced from retained evidence. That withdrawal has a consequence we intend to honour consistently.

An axis proposal is a pre-registration, not a report. The rubric above has not been executed. No builder has been assigned a level. No score exists. Where this page describes cohort behaviour it describes a pattern that vendor documentation makes possible, never a measurement of who does what. A reader should not infer from the specificity of the protocol that a run has happened. It has not.

How this relates to our existing axes

This axis is adjacent to published work, and we want the boundaries stated rather than assumed.

It is not our input-validation and data-integrity work. That axis asks whether an emitted write endpoint rejects a malformed JSON body at the server boundary, and it is scored with a payload battery of text fields. This one starts where that one stops: opaque bytes, a claim about those bytes, and a second service with its own authorization model. A project could score at the top of the input-validation axis and still write every user's uploads into one shared, mutually readable bucket.

It is not our auth and access-control work. That axis asks whether the platform offers documented per-row isolation primitives at all. This one takes the primitive as given and asks whether the emitted policy actually uses it to scope an object to its owner. Capability and application are different questions and a builder can pass one while failing the other.

It is not our runtime lock-in work. That axis includes file storage in its question about whether stateful services can be repointed at your own infrastructure. Portability of the storage service and safety of the upload path are orthogonal: a perfectly repointable bucket can be perfectly open.

It is not our rate-limiting work. Throttling asks how many requests reach an endpoint. This asks what one request is permitted to write and who may read it afterwards.

Limitations and open questions

  • Single-generation variance. One export is one sample. A builder that is inconsistent between generations needs a multi-run design before scoring is fair.
  • Prompt sensitivity. A prompt that mentions private files would measure instruction-following rather than default posture. The reference prompt describes the feature, not its defenses, deliberately.
  • Out-of-repo evidence. Signal 1 frequently depends on a policy stored in a backend console. We have a rule for reading it and recording provenance. We do not yet have a good rule for a builder that neither exports the policy nor exposes it, and we would rather solve that in the open.
  • Weight subjectivity. The 25/20/15/15/15/10 split is a proposal. A reasoned argument that serving posture should outweigh content verification would change the ranking, and we would rather have that argument now than after publishing scores.
  • Malware scanning is deliberately unscored. OWASP recommends running an upload "through an antivirus or a sandbox if available". We left it out of the rubric because no builder in the cohort documents it, and a signal on which every entrant scores zero measures nothing while making the total look more thorough than it is. If that changes, it should be added.

Comments, counter-rubrics and reproduction attempts are welcome. A run of the battery that contradicts what this page predicts is the single most useful thing you can send us.

References

B

Written by

BuilderProof Editorial Team

The BuilderProof lab publishes reproducible, community-editable benchmarks and methodology proposals for AI app builders. Axes are scored from documentation-derived rubrics and open to public revision.

Cite this benchmark

Plain text
BuilderProof Editorial Team. "File-Upload and Media-Handling Posture: A Proposed Axis for What an AI Builder's Upload Path Actually Permits (August 2026)". BuilderProof, August 2026. https://www.builderproof.org/benchmarks/file-upload-media-handling-posture-axis-proposal-august-2026.
BibTeX
@misc{builderproof-file-upload-media-handling-posture-axis-proposal-august-2026,
  title  = {{File-Upload and Media-Handling Posture: A Proposed Axis for What an AI Builder's Upload Path Actually Permits (August 2026)}},
  author = {{BuilderProof editorial team}},
  year   = {2026},
  month  = {aug},
  howpublished = {\url{https://www.builderproof.org/benchmarks/file-upload-media-handling-posture-axis-proposal-august-2026}},
  note   = {BuilderProof, builderproof.org}
}

Frequently asked questions

Do AI app builders leave storage buckets publicly readable?

At the platform layer, in August 2026, mostly no. Supabase documents that by default Storage does not allow any uploads to buckets without RLS policies, and Lovable documents that public storage buckets are blocked by default on all plans. Those are fail-closed defaults. The residual risk sits one layer down, in the shape of the access policy the builder writes in order to get past that default, which is why this axis scores the emitted policy rather than the bucket's public flag.

What is the difference between an authenticated storage policy and an authorized one?

An authenticated policy checks that the caller is signed in. An authorized policy checks that this particular object belongs to this particular caller, for example by comparing the first folder segment of the object key against the caller's user id, or by checking the object's owner_id. A policy that only requires authentication is fail-closed against strangers and completely open between users of the same app. It is nearly impossible to detect while testing, because a single-account app cannot tell the two policies apart.

Why is a bucket MIME-type restriction not a security control?

Because it filters a value the sender chooses. OWASP states that the Content-Type for uploaded files is provided by the user and as such cannot be trusted, as it is trivial to spoof. Supabase's allowedMimeTypes bucket setting is evaluated against exactly that client-supplied header, so it reliably prevents an honest mistake and does not stop a deliberate one. Full marks on this signal require the emitted server-side code to verify the file's signature against its declared type.

How large a file can an app built by an AI app builder accept?

It depends on an architectural choice the builder makes silently. If the upload is proxied through a server route on Vercel, the binding limit is a 4.5 MB request body, above which the function returns 413 FUNCTION_PAYLOAD_TOO_LARGE. If the browser uploads straight to Supabase Storage, Supabase describes its standard upload method as ideal for files not larger than 6MB and recommends TUS resumable uploads above that. Both ceilings sit below the size of an ordinary modern phone photo, and neither is visible in the generated interface.

How would you reproduce a file-upload posture score?

Generate a reference app from a fixed prompt with no manual edits, record the emitted upload path and the emitted storage policy including where the policy was read from, then create a second account and attempt to read and overwrite the first account's object. That second-account step is the one that separates a merely authenticated app from an owner-scoped one, and it is the step a builder's own workflow never performs. Then send a fixed battery of contradictory content types, oversize files, traversal filenames and repeat keys, and finally delete the owning record to see whether the object survives.

Has BuilderProof scored any builder on this axis?

No. This page is a pre-registration, not a report. The rubric has not been executed, no builder has been assigned a posture level, and no scores exist. After BuilderProof withdrew its June 2026 output-quality result set and the composite that depended on it, the lab publishes rubrics and protocols before results, and describes patterns that vendor documentation makes possible rather than measurements of which tool does what.

Methodology

How We Benchmark AI App Builders: The BuilderProof Methodology v1

BuilderProof methodology v1.1: the published rubric, brief OQ-7, environment standards and weights used to score AI app builders on output quality, speed, deploy quality and agency suitability. The four June 2026 result sets were withdrawn on August 21, 2026 as placeholder data, so the lab currently publishes method, not scores.

11 min read180