Skip to content

fix(artifacts): Port artifact reference scoping and path-segment validation to v1 - #6794

Open
GWeale wants to merge 3 commits into
v1from
backport-v1-pr04
Open

fix(artifacts): Port artifact reference scoping and path-segment validation to v1#6794
GWeale wants to merge 3 commits into
v1from
backport-v1-pr04

Conversation

@GWeale

@GWeale GWeale commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Ports three artifact-service fixes from main to v1. Stored artifacts stay reachable; the input rules below have no flag and no opt-out, so a caller sending a rejected value changes the value.

Scope artifact references to the caller (f8631500)

  • An artifact:// reference must name the caller's own app, user and session, checked on save and on load.
  • user: references still resolve across sessions of the same user.
  • In-memory service only; GCS has no reference support on v1.

Validate path segments in the in-memory and GCS services (45a77dc5, reland of 8718aeff after 961f3e88)

  • app_name, user_id and session_id must be non-empty, null-byte free, not rooted, not drive-qualified, and neither ., .., nor containing a .. segment; otherwise InputValidationError.
  • That is HTTP 400 on save artifact, HTTP 500 on load, list, delete and versions; handlers for those four are a follow-up.
  • FileArtifactService keeps its own stricter validator and treats an empty session_id as an error.

Reject rooted, drive-qualified and parent-referencing filenames (2716ad55)

  • A filename must not be rooted, drive-qualified (C:...), or contain a .. segment, on either platform's separators.
  • FileArtifactService also rejects the drive-relative C:name form for user_id and session_id.

Re-implemented rather than applied: 2716ad55.

GWeale added 3 commits August 17, 2026 22:48
InMemoryArtifactService accepted a caller-supplied artifact:// reference and,
on load, dereferenced whatever app, user and session were embedded in that URI
without ever comparing them to the caller's own. A caller could store a
reference naming another app or another user and read back bytes belonging to
that scope.

The service now validates a parsed reference against the caller's app, user
and session, both before storing it on save and before following it on load.
A reference that stays inside the same session still resolves, and a
user-scoped reference (one with no session in the URI) is still readable from
any session of the same user.

Behaviour change: storing or loading a reference that names a different app,
user or session now raises InputValidationError instead of silently resolving.
On the artifact save endpoint that surfaces as HTTP 400.

The GcsArtifactService half of the upstream change is not ported. On this
branch that service raises NotImplementedError for file_data on save and never
dereferences references on load, so it has no reference support to constrain.

Port of the upstream fix to the v1 branch.
…ces (v1)

InMemoryArtifactService and GcsArtifactService built their storage key by
interpolating app_name, user_id and session_id straight into a string, with no
check on any of them. An identifier that was empty, held a null byte, started
with a separator, was drive-qualified, or contained a ".." segment produced an
odd key rather than an error.

A shared validate_path_segment now lives in artifact_util.py and both services
call it before building a key, so the three artifact services agree on which
identifiers are acceptable and a future path-backed backend inherits the
check. The helper is copied from the upstream branch unchanged, including its
isinstance(value, str) guards: callers outside the annotation do reach it, and
the accompanying test pins that a non-string value passes rather than raising
a TypeError.

This is defence in depth, not a cross-tenant fix. Both services key on a flat
string, so ".." never traverses anything, and an embedded separator remains
accepted here exactly as it is upstream.

FileArtifactService keeps its own stricter private validator, which also
rejects embedded path separators. The upstream consolidation onto the shared
helper is deliberately not ported, because on this branch it would relax that
check and start accepting a user_id such as "has/slash" on the one service
that writes real filesystem paths.

Behaviour changes: an app_name, user_id or session_id that is empty, holds a
null byte, starts with a separator, is drive-qualified, or contains a ".."
segment now raises InputValidationError on the in-memory and GCS services. On
the artifact save endpoint that is HTTP 400; the load, list, delete and
version endpoints have no handler for it, so there it becomes HTTP 500. Adding
those handlers is left out of this change. Separately, an empty-string
session_id passed to FileArtifactService is now an error rather than being
treated as no session at all.

Port of two upstream commits to the v1 branch, landed as one helper so the
weaker intermediate form is never on this branch.
…t filenames (v1)

FileArtifactService decided whether a caller-supplied filename was safe by
joining it under the scope root, resolving the result, and checking that the
resolved path was still inside the root. Two shapes got through. A
drive-qualified name such as "C:\Windows\evil.txt" was converted to the
relative "C:/Windows/evil.txt", which is not absolute, so it passed the guard;
on Windows, joining it replaces the drive and the write lands outside the
root. And "folder/../alias.txt" resolved back inside the root and was
accepted, so two different filenames addressed one artifact.

_resolve_scoped_artifact_path now rejects rooted, drive-qualified and
parent-referencing filenames up front, before any joining or resolving. The
whole function is replaced rather than patched, which also brings in the
Windows path normalization that landed upstream separately; the two are
entangled in the same few lines and splitting them would leave a form that is
on neither branch.

The file service's own private identifier validator gains the same
drive-qualified check. It rejected "C:/x" only as a side effect of its
separator rule and accepted a bare "C:evil", which on Windows escapes the root
directory by the same join. That guard is not in the upstream commit; upstream
had already deleted this validator in favour of the shared one, which this
branch deliberately does not do.

Behaviour change: a filename such as "folder/../alias.txt" that resolves back
inside the scope is now rejected rather than silently aliased to
"alias.txt". Anyone relying on that aliasing gets an InputValidationError.

Port of the upstream fix to the v1 branch.
@GWeale GWeale changed the title fix(artifacts): Port artifact reference scoping and path validation to v1 fix(artifacts): Port artifact reference scoping and path-segment validation to v1 Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants