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.

