From ff17459760ebc537f7e90724f55810a53bb9acb3 Mon Sep 17 00:00:00 2001 From: Ben Peter Date: Thu, 16 Jul 2026 15:55:14 +0200 Subject: [PATCH 1/2] test: assert client-supplied guid is validated before it becomes a file id A source PUT can supply a guid form field. On a new document that guid becomes the file id and is written into the reserved .da-versions key space. These tests assert that a guid which is not a plain UUID, or which contains a slash or a .da-versions segment, is rejected with 400 and never written to storage, while a valid UUID guid still works. The tests are red against current code. --- test/storage/version/put.test.js | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index 3b7e9a13..027e4aff 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -608,6 +608,66 @@ describe('Version Put', () => { assert.equal('myidAAA', resp.metadata.id); }); + describe('client-supplied guid validation on creation', () => { + async function loadPut(s3Sent) { + const mockGetObject = async () => ({ status: 404 }); + const mockS3Client = { + send: (c) => { + s3Sent.push(c); + return { $metadata: { httpStatusCode: 200 } }; + }, + }; + return esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { default: mockGetObject }, + '../../../src/storage/utils/version.js': { ifNoneMatch: () => mockS3Client }, + }); + } + + const badGuids = [ + ['a non-UUID string', 'test-guid'], + ['a slash', 'foo/bar'], + ['a leading slash', '/etc/passwd'], + ['a .da-versions segment', 'aaa/.da-versions/bbb'], + ['a bare .da-versions segment', '.da-versions'], + ['a UUID with a trailing path', '11111111-1111-4111-8111-111111111111/x'], + ]; + + badGuids.forEach(([desc, guid]) => { + it(`rejects a guid with ${desc} with 400 and writes nothing`, async () => { + const s3Sent = []; + const { putObjectWithVersion } = await loadPut(s3Sent); + const update = { org: 'orgOne', key: '/root/somedoc.html', type: 'text/html' }; + const resp = await putObjectWithVersion( + {}, + { org: 'orgOne', ext: 'html' }, + update, + true, + guid, + ); + assert.equal(400, resp.status, `guid "${guid}" should be rejected with 400`); + assert.equal(0, s3Sent.length, `guid "${guid}" must not be written to storage`); + }); + }); + + it('accepts a valid UUID guid and uses it as the document id', async () => { + const s3Sent = []; + const { putObjectWithVersion } = await loadPut(s3Sent); + const guid = '9b2e6c1a-4f3d-4a2b-8c1e-1d2f3a4b5c6d'; + const update = { org: 'orgOne', key: '/root/somedoc.html', type: 'text/html' }; + const resp = await putObjectWithVersion( + {}, + { org: 'orgOne', ext: 'html' }, + update, + true, + guid, + ); + assert.equal(201, resp.status); + assert.equal(guid, resp.metadata.id); + assert.equal(1, s3Sent.length); + assert.equal(guid, s3Sent[0].input.Metadata.ID); + }); + }); + it('Post Object With Version creates new version', async () => { const req = { json: () => ({ From c83f990a6c019fac12fe4955b05370c60b6af194 Mon Sep 17 00:00:00 2001 From: Ben Peter Date: Thu, 16 Jul 2026 16:07:55 +0200 Subject: [PATCH 2/2] fix: validate the client-supplied document guid before it becomes a file id A source PUT or POST may include a guid form field. When the target has no stored file id yet, that guid becomes the document file id. The file id is then used as a path segment inside the reserved .da-versions key space. Before this change the guid was accepted without any checks. A value with a slash or a .da-versions segment, or any other value that is not a plain UUID, was stored as the file id and used to build keys. This change validates the guid against a UUID allowlist so only a plain UUID is accepted as a new file id. A missing or empty guid still falls back to a generated UUID, so normal writes are unaffected. A file id read back from stored metadata is also treated as untrusted. An id persisted before this validation existed could contain key steering characters. The storage transport builds a URL from the key, so it collapses a dot or double dot segment, treats a backslash as a separator, decodes a percent escape, and strips whitespace. A stored id that uses any of those, or that has a slash or a .da-versions segment, is now refused before it is used to build reserved keys. This check is weaker than the UUID allowlist on purpose, so a benign legacy single-segment id keeps working. Document the optional guid field, its UUID constraint, the 400 response, and the 409 response for a mismatch against an existing stored identifier on the source API, and regenerate the API docs. Add unit, integration, and OpenAPI contract tests for the new validation. --- docs/index.html | 1264 +++++++++++++++------------- docs/openapi/schemas.yaml | 10 + docs/openapi/source-api.yaml | 16 + package-lock.json | 1 + package.json | 1 + src/storage/version/paths.js | 39 + src/storage/version/put.js | 15 +- test/docs/openapi.test.js | 53 ++ test/it/it-tests.js | 57 ++ test/storage/version/paths.test.js | 74 ++ test/storage/version/put.test.js | 69 +- 11 files changed, 993 insertions(+), 606 deletions(-) create mode 100644 test/docs/openapi.test.js diff --git a/docs/index.html b/docs/index.html index 6b3cfbdb..4c3a5eb0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,413 +12,413 @@ margin: 0; } - -

DA Admin API (1.0-RC)

Download OpenAPI specification:

License: Apache 2.0

DA Admin API is used to manage Document Authoring content repositories.

-

Source

DA Admin API (1.0-RC)

Download OpenAPI specification:

License: Apache 2.0

DA Admin API is used to manage Document Authoring content repositories.

+

Source

The source API allows you to work directly with your content sources (Docs, Sheets, Media).

-

Get source content

Get source content

Get a content source from an organization.

+" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

Get a content source from an organization.

Important: For files, the path parameter must include the file extension (e.g., myfile.html, data.json). The full URL would be: /source/{org}/{repo}/myfile.html

-
Authorizations:
bearer
path Parameters
org
required
string

The organization.

-
repo
required
string

Name of the repository.

-
path
required
string

Path to the source content.

-

Responses

Response samples

Content type
<body>
+
Authorizations:
bearer
path Parameters
org
required
string

The organization.

+
repo
required
string

Name of the repository.

+
path
required
string

Path to the source content.

+

Responses

Response samples

Content type
<body>
   <header></header>
   <main>
     <div></div>
   </main>
 </body>
-

Create or replace source content

Create or replace source content

Create or replace a content source within an organization.

+" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

Create or replace a content source within an organization.

Accepts either a multipart/form-data body with the content in the data field, or a raw request body for text-based types (text/html, application/json).

+

The multipart/form-data body may also include an optional guid field. When the target +location does not already have a stored identifier, guid becomes the document identifier. +It must be a UUID. A guid that is not a UUID is rejected with a 400 response. When the +target already has a stored identifier that differs from guid, the request is rejected with +a 409 response. When omitted, the service generates a UUID.

Important: For files, the path parameter must include the file extension (e.g., myfile.html, data.json). For folders, omit the extension (e.g., myfolder).

-
Authorizations:
bearer
path Parameters
org
required
string

The organization.

-
repo
required
string

Name of the repository.

-
path
required
string

Path to the source content.

-
Request Body schema:
data
string <binary>

The content to store at the specified location.

-

Responses

Request samples

Content type
destination: '/aemsites/geometrixx/path/to/file.html'
-

Response samples

Content type
application/json
{}

Create source content

Authorizations:
bearer
path Parameters
org
required
string

The organization.

+
repo
required
string

Name of the repository.

+
path
required
string

Path to the source content.

+
Request Body schema:
data
string <binary>

The content to store at the specified location.

+
guid
string <uuid>

Optional client-supplied identifier for the document. When the target +location does not already have a stored identifier, this value becomes +the document identifier. It must be a UUID. A value that is not a UUID +is rejected with a 400 response. When the target already has a stored +identifier that differs from this value, the request is rejected with a +409 response. When omitted, the service generates a UUID.

+

Responses

Request samples

Content type
destination: '/aemsites/geometrixx/path/to/file.html'
+

Response samples

Content type
application/json
{}

Create source content

Create a content source within an organization.

+" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

Create a content source within an organization.

Accepts either a multipart/form-data body with the content in the data field, or a raw request body for text-based types (text/html, application/json).

+

The multipart/form-data body may also include an optional guid field. When the target +location does not already have a stored identifier, guid becomes the document identifier. +It must be a UUID. A guid that is not a UUID is rejected with a 400 response. When the +target already has a stored identifier that differs from guid, the request is rejected with +a 409 response. When omitted, the service generates a UUID.

Important: For files, the path parameter must include the file extension (e.g., myfile.html, data.json). For folders, omit the extension (e.g., myfolder). Examples:

@@ -515,24 +549,38 @@
  • File: /source/{org}/{repo}/myfile.html
  • Folder: /source/{org}/{repo}/myfolder
  • -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -
    Request Body schema:
    data
    string <binary>

    The content to store at the specified location.

    -

    Responses

    Request samples

    Content type
    destination: '/aemsites/geometrixx/path/to/file.html'
    -

    Response samples

    Content type
    application/json
    {}

    Delete source content

    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +
    Request Body schema:
    data
    string <binary>

    The content to store at the specified location.

    +
    guid
    string <uuid>

    Optional client-supplied identifier for the document. When the target +location does not already have a stored identifier, this value becomes +the document identifier. It must be a UUID. A value that is not a UUID +is rejected with a 400 response. When the target already has a stored +identifier that differs from this value, the request is rejected with a +409 response. When omitted, the service generates a UUID.

    +

    Responses

    Request samples

    Content type
    destination: '/aemsites/geometrixx/path/to/file.html'
    +

    Response samples

    Content type
    application/json
    {}

    Delete source content

    Delete the content source or directory.

    +" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

    Delete the content source or directory.

    Important: The path parameter works as follows:

    • For files: Include the extension in the path (e.g., myfile.html)
    • @@ -550,23 +598,23 @@
    • Delete file: /source/{org}/{repo}/myfile.html
    • Delete folder: /source/{org}/{repo}/myfolder
    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -

    Responses

    Copy

    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +

    Responses

    Copy

    The copy API allows you to copy a directory or source.

    -

    Copy source content

    Copy source content

    Copy a content source within an organization.

    +" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

    Copy a content source within an organization.

    Important: The path parameter works as follows:

    • For files: Include the extension in the path (e.g., myfile.html)
    • @@ -584,26 +632,26 @@
    • Copy file: /copy/{org}/{repo}/myfile.html
    • Copy folder: /copy/{org}/{repo}/myfolder
    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -
    Request Body schema: multipart/form-data
    destination
    string

    The destination path of the copy.

    -

    Responses

    Request samples

    Content type
    multipart/form-data
    destination: '/aemsites/geometrixx/path/to/file.html'
    -

    Move

    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +
    Request Body schema: multipart/form-data
    destination
    string

    The destination path of the copy.

    +

    Responses

    Request samples

    Content type
    multipart/form-data
    destination: '/aemsites/geometrixx/path/to/file.html'
    +

    Move

    The move API allows you to move a directory or source.

    -

    Move source content

    Move source content

    Move a content source within an organization.

    +" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

    Move a content source within an organization.

    Important: The path parameter works as follows:

    • For files: Include the extension in the path (e.g., myfile.html)
    • @@ -621,141 +669,153 @@
    • Move file: /move/{org}/{repo}/myfile.html
    • Move folder: /move/{org}/{repo}/myfolder
    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -
    Request Body schema: multipart/form-data
    destination
    string

    The destination path of the move.

    -

    Responses

    Request samples

    Content type
    multipart/form-data
    destination: '/aemsites/geometrixx/path/to/file.html'
    -

    Version

    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +
    Request Body schema: multipart/form-data
    destination
    string

    The destination path of the move.

    +

    Responses

    Request samples

    Content type
    multipart/form-data
    destination: '/aemsites/geometrixx/path/to/file.html'
    +

    Version

    The versionsource and versionlist APIs allow you to work with versions of source documents and sheets.

    -

    Get source version

    Get source version

    Returns version of a given source.

    +" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

    Returns version of a given source.

    Important: The guid parameter must include the file extension (e.g., version-123.html, v2.json). Example: /versionsource/{org}/{repo}/version-123.html

    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    guid
    required
    string

    GUID of the version source.

    -

    Responses

    Response samples

    Content type
    <body>
    +
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    guid
    required
    string

    GUID of the version source.

    +

    Responses

    Response samples

    Content type
    <body>
       <header></header>
       <main>
         <div></div>
       </main>
     </body>
    -

    Create source version

    Create source version

    Create a version of a given source.

    +" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

    Create a version of a given source.

    Important: The path parameter must include the file extension (e.g., myfile.html, data.json). Example: /versionsource/{org}/{repo}/myfile.html

    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -

    Responses

    Response samples

    Content type
    application/json
    [
    • {
      }
    ]

    List source versions

    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +

    Responses

    Response samples

    Content type
    application/json
    [
    • {
      }
    ]

    List source versions

    Returns a list of versions for a given source.

    +" class="sc-iJSMbW sc-cBEgGa fiNpIH ewCFMV">

    Returns a list of versions for a given source.

    Important: The path parameter must include the file extension (e.g., myfile.html, data.json). Example: /versionlist/{org}/{repo}/myfile.html

    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -

    Responses

    Response samples

    Content type
    application/json
    [
    • {
      }
    ]

    List

    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +

    Responses

    Response samples

    Content type
    application/json
    [
    • {
      }
    ]

    List

    The list API will list children of a directory.

    -

    List sources

    Returns a list of sources from an organization.

    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -
    header Parameters
    da-continuation-token
    string

    Continuation token from the previous list response header.

    -

    Responses

    Response samples

    Content type
    application/json
    {
    • "sources": [
      ]
    }

    Config

    List sources

    Returns a list of sources from an organization.

    +
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +
    header Parameters
    da-continuation-token
    string

    Continuation token from the previous list response header.

    +

    Responses

    Response samples

    Content type
    application/json
    {
    • "sources": [
      ]
    }

    Config

    The config API will allow you to associate sheet-formatted configurations with orgs, sites, directories, documents, and sheets.

    -

    Config

    Get a config for an org, repo, or source.

    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -

    Responses

    Response samples

    Content type
    application/json
    {
    • "total": 0,
    • "limit": 0,
    • "offset": 0,
    • "data": [
      ],
    • ":type": "string"
    }

    New config

    Create a config for an org, repo, or source.

    -
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    -
    repo
    required
    string

    Name of the repository.

    -
    path
    required
    string

    Path to the source content.

    -
    Request Body schema: multipart/form-data
    data
    string <binary>

    The content to store at the specified location.

    -

    Responses

    Request samples

    Content type
    multipart/form-data
    destination: '/aemsites/geometrixx/path/to/file.html'
    -

    Response samples

    Content type
    application/json
    {}
    +

    Config

    Get a config for an org, repo, or source.

    +
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +

    Responses

    Response samples

    Content type
    application/json
    {
    • "total": 0,
    • "limit": 0,
    • "offset": 0,
    • "data": [
      ],
    • ":type": "string"
    }

    New config

    Create a config for an org, repo, or source.

    +
    Authorizations:
    bearer
    path Parameters
    org
    required
    string

    The organization.

    +
    repo
    required
    string

    Name of the repository.

    +
    path
    required
    string

    Path to the source content.

    +
    Request Body schema: multipart/form-data
    data
    string <binary>

    The content to store at the specified location.

    +
    guid
    string <uuid>

    Optional client-supplied identifier for the document. When the target +location does not already have a stored identifier, this value becomes +the document identifier. It must be a UUID. A value that is not a UUID +is rejected with a 400 response. When the target already has a stored +identifier that differs from this value, the request is rejected with a +409 response. When omitted, the service generates a UUID.

    +

    Responses

    Request samples

    Content type
    multipart/form-data
    destination: '/aemsites/geometrixx/path/to/file.html'
    +

    Response samples

    Content type
    application/json
    {}