diff --git a/.changeset/25077-rest-docs-viewer-attr-escape.md b/.changeset/25077-rest-docs-viewer-attr-escape.md new file mode 100644 index 00000000000..f048f0606fe --- /dev/null +++ b/.changeset/25077-rest-docs-viewer-attr-escape.md @@ -0,0 +1,9 @@ +--- +'@objectstack/rest': patch +--- + +fix(rest): the API docs viewer escapes the request-derived openapi URL for the attribute it lands in + +The `GET {base}/docs` viewer page builds the sibling openapi URL from the request and places it into a `data-url="…"` attribute of a script element. It interpolated that value into the attribute with no escaping, so a character the attribute context interprets reached the served markup as itself rather than as the matching HTML entity. The value is now escaped for its attribute context (ruling 3C on #25040: every outlet that renders a value into HTML escapes it by the context it lands in): the five characters a quoted attribute or element text interprets become their entities, and a browser decodes each back to the original character for the page script. A URL carrying none of those characters is served byte-identically. + +Clause-②: no diff --git a/packages/rest/src/discovery-routes.ts b/packages/rest/src/discovery-routes.ts index 8bf688c0d6d..2b1c7eb3fb2 100644 --- a/packages/rest/src/discovery-routes.ts +++ b/packages/rest/src/discovery-routes.ts @@ -67,6 +67,8 @@ // half is the class's `probeMcpServeable`, reached through the host. import { isMcpServerEnabled } from '@objectstack/types'; import { handleRouteError, sendThrownError } from './error-response.js'; +// [#25077] The docs viewer page escapes the value it places into its attribute. +import { escapeHtml } from './html-escape.js'; import { logError } from './log.js'; // [#5040 E6] The `api`-metadata endpoints the OpenAPI document folds in. import { enrichOpenApiWithEndpoints } from './openapi-endpoints.js'; @@ -690,6 +692,10 @@ export function registerOpenApiEndpoints(this: OpenApiRouteHost, basePath: strin // Strip the trailing /docs to get the API base. const apiBase = reqPath.replace(/\/docs\/?$/, ''); const specUrl = `${apiBase}/openapi.json`; + // [#25077] Escaped for the attribute it lands in (ruling 3C on + // #25040: every outlet that renders a value into HTML escapes it + // by context). A path without HTML-special characters renders + // byte for byte as before. `html-outlets.pin.test.ts` pins both. const html = `
@@ -698,7 +704,7 @@ export function registerOpenApiEndpoints(this: OpenApiRouteHost, basePath: strin