On Sat, Aug 15, 2026 at 6:50 PM Jordan Peck <[email protected]> wrote:

> While I think this is now a complete fix to 3445 it's still not
> structurally
> solved. A new HTTP message type could be add to SVN for a new feature
> that doesn't fit the expected format of this still basic string find and
> replace.
> An XML aware proxy is the structurally sound fix, but I really don't think
> there's any reason to do it now, given how large a task it would be.
> Keeping future message type's proxy compliant should be fairly easy with
> the whitelists and some awareness, which hopefully the expanded proxy
> tests bring.

Jordan,

I haven't suddenly found any more time, but it's been weighing on me that I
started interacting with you on this topic and haven't been able to see it
through.  It hasn't helped that, as you've noticed, the problem space
seemed to keep expanding and, with it, the footprint of the solution.  But
I've also had kinda stuck in my mind the original solution that I mused
about in SVN-3445:

   Have the slave server drop a header into the proxied request that the
> master
>    looks for.  The header tells the master server what the slave's
> "localpath"
>    is.  The master, upon seeing that header, then uses the slave's
> localpath
>    instead of its own paths when generating responses.  Is a header which
> can
>    essentially cause a server to lie about its locations a bad/insecure
> thing?

So I asked Claude about the pros and cons of such a thing.  The response is
largely what I suspected, but I'll present it below just "for the record".
I found it interesting that its "middle ground" solution is the same
conclusion you drew -- the need for a structure-aware filter (that wouldn't
need to do arguably sneaky things like looking for "<D:href>/slave").

-- Mike
*::: CLAUDE RESPONSE :::*

Your proposal is the standard pattern for this problem. It’s essentially
what X-Forwarded-Prefix does (Spring, Traefik, and others honor it) and
what SCRIPT_NAME does in WSGI: the proxy tells the backend what base path
the client sees, and the backend generates URLs against that. So you’re in
well-trodden territory, which is itself a point in its favor.

*Pros*

   - *Correct by construction.* The origin knows which strings are paths
   and which are user data, because it’s generating them. The edge has to
   guess. That eliminates the false-positive munging problem entirely.
   - *Catches what string replacement misses.* Percent-encoded paths,
   XML-escaped paths, paths split across attributes, absolute vs. relative
   forms, and the HTTP Location header all come out right if the origin’s
   URL builder is the single point of generation.
   - *Cheaper and simpler at the edge.* No buffering or parsing response
   bodies, so streaming and large responses pass through untouched, and the
   edge proxy stays dumb.
   - *Scales to multiple edges.* Each edge passes its own prefix per
   request, so you can have several edges mounted at different paths with no
   per-edge config on the origin.

*Cons and risks*

   - *Trust boundary.* The origin must honor the header only from trusted
   edges, and the edge must strip any client-supplied copy. Otherwise a client
   can make the origin emit arbitrary paths, which is a minor but real
   link-injection/redirect vector. Typically you’d use mTLS, a shared secret,
   or a network allowlist.
   - *Requires origin changes, and completeness matters.* Every place the
   origin emits a path has to go through the prefix-aware builder. Any
   hardcoded path you miss becomes a subtle bug that only shows up behind a
   proxy, which is the worst place to discover it. Grep for literal path
   strings, and add a test that runs behind a fake prefix.
   - *Paths must be rendered at response time, not persisted.* This is the
   big one given your sync mechanism. If the origin stores any of that
   metadata, for example by writing the resource’s own href into the stored
   document, then the edge’s prefix gets persisted on the origin. It then
   shows up for direct origin clients, and gets synced back to the edge as
   data. The pattern only works cleanly if stored state holds path-relative or
   ID-only references and the envelope is composed at serialization time.
   - *Out-of-request emissions don’t get the header.* Anything the origin
   generates outside a proxied request context won’t know the edge’s prefix:
   async job completions, webhooks/callbacks, notifications, links in emails,
   and log messages.
   - *Caching.* If anything between the edge and origin caches responses,
   it must vary on that header, or one edge’s paths get served to another.
   - *Prefix vs. mapping.* This works if the difference is a pure prefix
   swap (/edge/api/... ↔ /origin/api/...). If the path structure differs
   beyond the prefix, you need a richer contract than a single header.
   - *Version skew.* An older origin that ignores the header silently
   produces origin paths. A response marker (e.g., the origin echoing back the
   prefix it applied) lets the edge detect and log that instead of failing
   quietly.

If origin changes aren’t feasible, there’s a middle ground: a
structure-aware filter at the edge that parses the XML and rewrites only
the known metadata elements and attributes, never user-data content. It’s
more brittle than your approach because the edge has to track the schema,
but it avoids blind string replacement. Still, if you control the origin,
your proposal is the better design, provided the “rendered at response
time, not stored” condition holds.

Reply via email to