User Controlled Ownership Field

ID

swift.user_controlled_ownership_field

Severity

high

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Access Control

Language

Swift

Tags

ASVS50:v8.1.1, CWE:639, CWE:915, NIST.SP.800-53, OWASP:2025:A01, PCI-DSS:6.5.8

Description

Data that came from the request reaches the property of a persisted model that says whose record it is — the customer, the owner, the submitter, whatever this application calls it. The caller therefore decides which account a record belongs to: they can create a record under somebody else’s name, or move one of somebody else’s to themselves.

A Fluent model has no setters, so the owner is written by assignment, by initializer, or not named at all. The detector reports:

  • the property assignment, receipt.holderID = try req.content.get(UUID.self, at: "holderID");

  • the owner written through its Fluent parent, expense.$submitter.id = input.submitterID;

  • the initializer argument, Expense(amount: input.amount, submitterID: input.submitterID), under the field’s own label or the <parent>ID label Fluent models use for a parent’s identifier;

  • a whole-body decode into the model, let expense = try req.content.decode(Expense.self), where the owner is never named because every field of the model is decoded from the request.

Rationale

This is CWE-915 (mass assignment) read as CWE-639 (authorization bypass through a user-controlled key): the property being assigned is not just any attribute, it is the one the authorization decision is later made on. Every subsequent "is this record yours?" check answers yes, because the request already said so.

What is mined and what needs a fact

Mined from the code

That the value is request-derived, and that it reaches the write: both are decided by the tainting analysis against the shipped Vapor library metadata. No profile is involved.

Needs a profile fact

Which property denotes the model’s owner. Nothing in a rule catalogue can express it, because the next application calls the property something else; the application’s own convention profile names it (ownershipFields), and that fact supplies this rule’s sink.

Without that fact this rule has no sink and reports nothing, and says so in the scan log rather than reporting a clean result it could not establish. Every finding carries the fact it used and the file:line where that fact can be checked.

A decode followed by setting the owner from the authenticated caller is not reported: the value the request carried for the owner never survives.

What the detector cannot see

The model has to be nameable at the point of the write: a parameter, a typed local, or a local initialized from the model’s initializer, a Fluent lookup or a decode. A write through self inside the model’s own initializer is the model’s plumbing and is not reported; the call that hands the initializer a request value is.

Remediation

Never let the request choose the owner. Set it from the authenticated user on the server side, and decode the request into a shape that has no owner field:

struct ExpenseInput: Content {
    var amount: Int
    var memo: String
}

func submit(req: Request) async throws -> Expense {
    let caller = try req.auth.require(Employee.self)
    let input = try req.content.decode(ExpenseInput.self)
    let expense = Expense()
    expense.amount = input.amount
    expense.memo = input.memo
    expense.$submitter.id = try caller.requireID() // server-derived, not from the request
    try await expense.save(on: req.db)
    return expense
}

Validating the submitted value against the caller is a weaker fix than not accepting it, but it is a fix — reject the request when the submitted owner is not the caller.

Configuration

The rule is driven by the ownershipFields facts of the application’s convention profile, which the profiler establishes and which travel with the scan. With --ai-mode off (the default), or with a profile that names no owner field, the rule is inert.

© 2026 Xygeni. All rights reserved