Skip to content

How-to: File Sharing API

FT reference: FT303 (NENE2-FT/filelog) — File sharing API: private files return 404 (not 403) to non-owners, owner-only delete/visibility-change, view-share vs edit-share permission tiers, body user_id ignored (ownership from header), name length limit 255, size is_int() strict, VULN-A〜L all SAFE, 59 tests / 82 assertions PASS.

This guide shows how to build a file metadata API where users own files, control visibility, and share access with other users at view or edit level.

Schema

sql
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    created_at TEXT NOT NULL
);

CREATE TABLE files (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id     INTEGER NOT NULL,
    name        TEXT    NOT NULL,
    size        INTEGER NOT NULL DEFAULT 0 CHECK (size >= 0),
    mime_type   TEXT    NOT NULL,
    description TEXT,
    visibility  TEXT    NOT NULL DEFAULT 'private'
                        CHECK (visibility IN ('private', 'public')),
    created_at  TEXT    NOT NULL,
    updated_at  TEXT    NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE file_shares (
    id                  INTEGER PRIMARY KEY AUTOINCREMENT,
    file_id             INTEGER NOT NULL,
    shared_with_user_id INTEGER NOT NULL,
    can_edit            INTEGER NOT NULL DEFAULT 0 CHECK (can_edit IN (0, 1)),
    created_at          TEXT    NOT NULL,
    UNIQUE (file_id, shared_with_user_id),
    FOREIGN KEY (file_id) REFERENCES files(id),
    FOREIGN KEY (shared_with_user_id) REFERENCES users(id)
);

Two-tier sharing: can_edit = 0 (view-only) and can_edit = 1 (edit access). UNIQUE(file_id, shared_with_user_id) prevents duplicate share entries.

Endpoints

MethodPathAuthDescription
POST/filesX-User-IdUpload file metadata
GET/filesX-User-IdList own files
GET/files/{fileId}X-User-IdGet file (visibility check)
PUT/files/{fileId}X-User-IdUpdate file (owner or edit-share)
DELETE/files/{fileId}X-User-IdDelete file (owner only)
POST/files/{fileId}/sharesX-User-Id (owner)Add share
DELETE/files/{fileId}/shares/{userId}X-User-Id (owner)Remove share

Private File → 404 (Not 403)

php
// Non-owner cannot see private files — 404 hides existence
if ($file['visibility'] === 'private') {
    $share = $this->repo->findShare($fileId, $userId);
    if ($share === null) {
        return $this->problems->create($request, 'not-found', 'File not found', 404);
    }
}

Private files return 404 to non-owners and non-sharers. Returning 403 would reveal the file exists. Public files return 200 to all authenticated users.

Ownership From Header — Ignore Body user_id

php
$userId = $this->requireUserId($request);
// ... validation ...
$id = $this->repo->create($userId, $name, $size, $mimeType, $description, $visibility, $now);

The file's user_id is always taken from the X-User-Id header. Any user_id in the request body is silently ignored. This prevents ownership injection attacks (VULN-E).

View-Share vs Edit-Share — Two Tiers

php
// Owner can always edit
$isOwner = ((int) $file['user_id']) === $userId;

if (!$isOwner) {
    $share = $this->repo->findShare($fileId, $userId);
    if ($share === null || !(bool) $share['can_edit']) {
        return $this->problems->create($request, 'forbidden', 'Edit access required', 403);
    }
}
  • Owner: all operations (read, write, delete, share management, visibility)
  • Edit-share (can_edit=1): can update name/size/mime/description — but NOT visibility
  • View-share (can_edit=0): read only — any write attempt → 403

Only owners can change visibility:

php
// Only owner can change visibility
if (!$isOwner && isset($body['visibility'])) {
    $visibility = (string) $file['visibility']; // silently ignore the request
}

Strict Input Validation

php
$size = $body['size'] ?? null;
if (!is_int($size) || $size < 0) {
    $errors[] = ['field' => 'size', 'code' => 'invalid', 'message' => 'size must be a non-negative integer'];
}

if (!is_string($name) || strlen($name) > 255 || $name === '') {
    $errors[] = ['field' => 'name', 'code' => 'invalid', 'message' => 'name required, max 255 chars'];
}
  • size: is_int() rejects floats like 1.5 (VULN-I)
  • name: max 255 characters — prevents oversized input crash (VULN-H)
  • visibility: in_array($value, ['private', 'public'], true) strict allowlist

Share Removal — Owner Only

php
// Only the file owner can remove shares
if ((int) $file['user_id'] !== $userId) {
    return $this->problems->create($request, 'not-found', 'File not found', 404);
}

The shared-with user cannot remove themselves from a share — only the owner can manage shares. Non-owners get 404 (not 403) to hide the file's existence (VULN-F).

User ID Validation — Reject Zero and Negative

php
$raw = $request->getHeaderLine('X-User-Id');
$userId = ctype_digit($raw) ? (int) $raw : 0;
if ($userId <= 0) {
    return $this->problems->create($request, 'unauthorized', 'Authentication required', 401);
}

X-User-Id: 0 and X-User-Id: -1 return 401 (VULN-L). Only positive integers are valid user IDs.


Vulnerability Assessment

V-01 — IDOR: private file accessible by other user ✅ SAFE

Risk: User B reads User A's private file. Finding: SAFE — private files return 404 to non-owners without a share entry.


V-02 — IDOR: delete other user's file ✅ SAFE

Risk: User B deletes User A's file. Finding: SAFE — delete checks ownership; non-owner gets 404. File still exists after failed attempt.


V-03 — IDOR: update other user's file ✅ SAFE

Risk: User B updates User A's file name/metadata. Finding: SAFE — update checks ownership; non-owner without edit-share gets 404.


V-04 — Privilege escalation: view-share attempts edit ✅ SAFE

Risk: User with view-only share calls PUT to modify the file. Finding: SAFE — edit check requires can_edit = 1; view-share returns 403.


V-05 — Ownership injection: user_id in request body ✅ SAFE

Risk: { "user_id": 99, "name": "..." } assigns file to user 99. Finding: SAFE — user_id from body is silently ignored; ownership always comes from X-User-Id header.


V-06 — Share removal by non-owner ✅ SAFE

Risk: Shared user removes themselves from the share list. Finding: SAFE — share delete endpoint checks file ownership; non-owner returns 404.


V-07 — SQL injection in name field ✅ SAFE

Risk: "name": "test'; DROP TABLE files; --" destroys data. Finding: SAFE — parameterized queries store the injection string as literal data. Files table intact.


V-08 — Oversized name causes crash ✅ SAFE

Risk: 300-character name causes DB error or memory exhaustion. Finding: SAFE — strlen($name) > 255 validation returns 422 before inserting.


V-09 — Float size type confusion ✅ SAFE

Risk: "size": 1.5 passes validation and corrupts size tracking. Finding: SAFE — is_int($size) rejects floats → 422.


V-10 — Edit-share escalates visibility to public ✅ SAFE

Risk: Edit-share user sets "visibility": "public" to expose a private file. Finding: SAFE — visibility changes are owner-only; edit-share's visibility field in PUT body is silently ignored.


V-11 — Private file existence disclosure via 403 ✅ SAFE

Risk: 403 response reveals the file exists even to unauthorized users. Finding: SAFE — non-owners receive 404, not 403. File existence is not disclosed.


V-12 — Auth bypass via X-User-Id: 0 or negative ✅ SAFE

Risk: X-User-Id: 0 or X-User-Id: -1 bypasses user check. Finding: SAFE — ctype_digit() + $userId <= 0 check returns 401 for zero and negative values.


VULN Summary

IDVulnerabilityFinding
V-01IDOR: private file access✅ SAFE
V-02IDOR: delete other user's file✅ SAFE
V-03IDOR: update other user's file✅ SAFE
V-04View-share privilege escalation✅ SAFE
V-05Ownership injection via body✅ SAFE
V-06Share removal by non-owner✅ SAFE
V-07SQL injection in name✅ SAFE
V-08Oversized name crash✅ SAFE
V-09Float size type confusion✅ SAFE
V-10Edit-share visibility escalation✅ SAFE
V-11Private file existence disclosure✅ SAFE
V-12Auth bypass via invalid user ID✅ SAFE

12 SAFE, 0 EXPOSED Private-file 404 pattern, header-only ownership, two-tier share permissions, strict type validation, and owner-only visibility prevent all IDOR and privilege escalation vectors.


What NOT to do

Anti-patternRisk
Return 403 for private file to non-ownerReveals file existence to unauthorized users
Accept user_id from request body for ownershipAny authenticated user claims ownership of any file
Allow view-share to call PUTShared viewers can modify file metadata
Allow edit-share to change visibilityShared editors expose private files to public
Allow shared user to remove their own shareUsers can revoke access management from the owner
Accept size: 1.5 (float)Type confusion; non-integer file sizes corrupt size tracking
No name length limitLong filenames can cause DB column overflow or memory issues
X-User-Id: 0 accepted as validUser ID 0 may match uninitialized rows or bypass ownership checks
ctype_digit() without > 0 check"0" passes ctype_digit but is not a valid user ID

Released under the MIT License.