# urljoin origin escape and misleading prefix checks

This case is a reusable, offline diagnostic for an agent that resolves pagination,
artifact or tool-result URL references against a trusted API origin. It uses only
the Python standard library and synthetic `.example` names. It never opens a URL.

## Failure, diagnosis and repair

`urljoin("https://trusted.example/api/jobs/", "//other.example/collect")`
selects a different host. An absolute reference has the same effect. Joining is
resolution, not authorization. A subsequent `startswith("https://trusted.example")`
still accepts a longer hostname, userinfo that points elsewhere, and a different
port. These are separate negative controls, not a hypothetical remote exploit.

The fixed `resolve_same_origin()` deliberately implements a small contract:

- Accept already encoded ASCII URI text, up to 2,048 characters. Reject raw
  whitespace, controls, backslashes, non-ASCII text and malformed percent escapes
  **before parsing** so parser normalization does not silently erase input.
- Resolve against the constant base; inspect the result, including `.port`.
- Require the exact `(https, trusted.example, 443)` origin. Reject userinfo and
  empty explicit ports. Port zero is not treated as an omitted port.
- Rebuild the authority using the validated fixed host. Preserve the encoded
  path and query and omit the fragment. Pass this result unchanged to any later
  component; further unquoting or URL joining changes the reviewed contract.

The fixture includes 13 permitted references and 23 rejected references. Examples
cover relative paths, query/fragment references, uppercase host, default port,
network-path references, foreign hosts, credentials, different and invalid ports,
controls, malformed authority and type errors. Normal inputs and rejection cases
each contribute exactly one main check. Seven harness assertions confirm that the
negative controls actually differ; they are **not** added to the 36 main checks.

## Run

Python 3.12 is the documented target. The execution record gives the measured
platform and patch version. No packages, account or API key are needed.

```sh
python -I probe.py
```

`probe.py` runs `pipeline.py broken`, `pipeline.py prefix`, and `pipeline.py fixed`
with a five-second bound per child. Broken and prefix return exit code 2 because
they violate the expected policy; fixed returns 0. JSON stdout, stderr and the
environment/hash manifest are saved in `evidence/`. The subprocesses inherit no
user environment; Windows receives only the OS bootstrap path from the Windows
API. This environment choice is not a security sandbox.

For integration, copy and adapt `resolve_same_origin` and its constants only after
reviewing whether this input contract fits the client. The full fixture is useful
when reviewing a proposed prefix check or a change in URL-handling libraries.

## Counterexamples and limits

This is **origin selection, not a complete SSRF defense or a path allowlist**.
`../../admin` and `/%2e%2e/admin` are intentionally permitted on the trusted origin.
Percent-encoded path/query data is opaque here. The fixture makes no claim that a
browser, proxy or server decodes it identically. A trusted-origin endpoint may
redirect or perform another request itself. Transport authorization, credentials,
DNS/IP checks, redirects, proxy behavior, network range checks and path permissions
must be reviewed at their respective layers. No HTTP server or HTTP client is
exercised; no inference about those layers follows from these passing checks.

The strict treatment of trailing-dot hosts, Unicode text and empty ports is a local
policy choice. It is not a claim that all such URIs are invalid. Properly encoded
Unicode paths may be accepted. Only the recorded fixed-origin fixture and runtime
are verified; other runtimes, IDNs, IPv6 origin policies and production traffic are
unverified. This is operator-created engineering evidence, not observed customer
demand, external AI usage or a population success rate.

## Official basis

Python documents absolute-reference replacement in `urljoin`, parser normalization,
and the need for application-specific validation. The policy and fixtures above
are our implementation choices, rather than an official recipe:

- [Python 3.12 urllib.parse.urljoin](https://docs.python.org/3.12/library/urllib.parse.html#urllib.parse.urljoin)
- [Python 3.12 URL parsing security](https://docs.python.org/3.12/library/urllib.parse.html#url-parsing-security)
- [Python 3.12 urlsplit and port behavior](https://docs.python.org/3.12/library/urllib.parse.html#urllib.parse.urlsplit)

The research review is in `source-review.json` and executable hashes are in
`evidence/record.json`. A future publication must retain the measured environment,
negative controls and these limits, and must not increase the existing site's
case/check totals until its own current source incorporates and publishes this case.
