Skip to content

Verify CKAN from a browser origin against ckan.tacc.utexas.edu — is CORS actually broken? #71

Closed

Description

@mosoriob

Question

The working assumption is that ckan.tacc.utexas.edu needs CORS configuration before ui-react can call it from the browser. That is a three-week-old guess and has never been checked. It may be a no-op.

Check it directly: load the app on a non-tacc.utexas.edu origin (a Vercel preview will do) and exercise /datasets/browse, /datasets/search, and a region's dataset list. Watch the network panel for blocked preflights and the console for CORS errors. A curl -I -H 'Origin: https://…' against a CKAN action endpoint settles the header question in seconds.

Record the verdict as: CORS is fine (close T6 unopened) or CORS is broken, with the exact failing request and the response headers CKAN actually sends.

Context

This surface is young and unexercised. The CKAN client landed in PR #61 on 2026-07-19, and PR #63 patched a URL-conflation bug in it the next day. Issue #59 tracks remaining DATA_CATALOG_API cleanup. config.ts defaults both DATA_CATALOG_API and DATA_CATALOG_BROWSE_URL to https://ckan.tacc.utexas.edu.

Activity

  1. added
    wayfinder:taskWayfinder ticket: manual work that unblocks a decision
    on Aug 8, 2026
  2. self-assigned this
    on Aug 8, 2026
  3. mosoriob commented on Aug 8, 2026

    @mosoriob
    ContributorAuthor

    Verdict: CORS is fine for production, broken for Vercel previews.

    The three-week-old guess was half right, and the half that matters is the one nobody expected.

    ckan.tacc.utexas.edu does send CORS headers, but Access-Control-Allow-Origin is echoed
    only for an allowlisted set of origins. Every other CORS header (-Allow-Methods,
    -Allow-Headers, -Expose-Headers, -Allow-Credentials, Vary: Origin) is sent
    unconditionally, to every origin, which makes the response look CORS-enabled at a glance.
    It is not — without Access-Control-Allow-Origin the browser blocks the read regardless.

    The allowlist, measured

    curl -o /dev/null -D - -H 'Origin: <o>' https://ckan.tacc.utexas.edu/api/3/action/status_show

    Origin Access-Control-Allow-Origin
    https://mint.tacc.utexas.edu echoed
    https://anything.tacc.utexas.edu echoed
    http://localhost:5173 / :3000 / no port echoed
    http://127.0.0.1:5173 echoed
    https://tacc.utexas.edu (apex) absent
    https://x.vercel.app absent
    https://mint.isi.edu absent
    https://evil.example.com absent

    So: *.tacc.utexas.edu (subdomains only, not the apex), plus localhost and 127.0.0.1 on
    any scheme and port. Nothing else.

    Uniform across endpoints — identical result on package_search (with and without ext_bbox),
    package_show, organization_list and status_show. It is an nginx origin match, not
    per-action logic. CKAN 2.11.5, Server: nginx.

    What this means for the app

    ckanAction in ui-react/src/lib/datasets/ckan.ts issues a plain GET with only
    Accept: application/json and no credentials. That is a CORS simple request — no preflight
    is involved, so the OPTIONS behaviour is a red herring. The request goes out, CKAN answers
    200, and the browser discards the body. It surfaces as a TypeError from fetch, not as an
    HTTP error.

    • Production (https://mint.tacc.utexas.edu) — works today. It matches the allowlist. No
      CORS change is needed from TACC, and none is needed from us.
    • Local dev and a locally-run container — work today. localhost and 127.0.0.1 are
      allowlisted, so npm run dev can be pointed straight at TACC's real CKAN.
    • Vercel previews — broken, and unfixable from our side. *.vercel.app is not allowlisted.

    Consequence 1: #74 is moot — closed unopened

    The nginx proxy would only ever serve the deployed container, and the deployed container sits
    at mint.tacc.utexas.edu, which is already allowlisted. It fixes a problem that does not exist
    there, and it cannot fix the one place that is actually broken (Vercel previews do not pass
    through that nginx). Closed with a pointer here.

    DATA_CATALOG_API and DATA_CATALOG_BROWSE_URL both stay pointed at the real host in #75.
    No new config value falls out of this ticket.

    Consequence 2: #76 cannot exercise /datasets/* from a Vercel preview

    #76 left this open as "exercised through whatever #74 lands on — or deferred to #77". Neither
    branch survives: #74 is dead, and deferring the entire data-catalog surface to the production
    flip means first contact with CKAN happens after the cutover.

    There is a third option #76 did not consider, and it is the cheap one: run the
    /datasets/* leg from localhost
    , which CKAN already allows. npm run dev (or the container
    run locally) pointed at TACC's real Hasura and CKAN exercises the real data on an allowlisted
    origin, with no proxy and no request to TACC. The rest of #76 stays on the Vercel preview as
    written. Noted on #76.

    This is a constraint on the test surface only. It is not a defect in ui-react, and it
    changes nothing about the production deployment.

  4. mosoriob commented on Aug 8, 2026

    @mosoriob
    ContributorAuthor

    Amendment (2026-08-08, later the same day): the Vercel gap is closed

    Max added ~^https://.*\.vercel\.app$ $http_origin; to the map $http_origin $cors_origin
    block on CKAN's nginx and reloaded. Re-measured and confirmed live:

    • https://ui-react-git-develop-mint.vercel.app now gets Access-Control-Allow-Origin echoed
      on package_search (with and without ext_bbox), package_show, organization_list and
      status_show, all 200, all with Vary: Origin.
    • The OPTIONS preflight also echoes it (204), so non-simple requests would work too.
    • Negative cases still rejected: http://x.vercel.app (wrong scheme), https://vercel.app
      (apex), https://x.vercel.app.evil.com (suffix spoof), https://tacc.utexas.edu (apex),
      https://evil.example.com.

    The verdict above is superseded for Vercel only. Everything else in it still holds — the
    allowlist mechanism, the "looks CORS-enabled but isn't" trap, the simple-request/no-preflight
    analysis, and the reason #74 stays closed
    (the container-nginx proxy was moot on its own terms: it only ever served the origin that
    already worked).

    The localhost workaround imposed on #76
    is lifted — the /datasets/* leg goes back onto the Vercel preview with the rest of the test.

    Caveat worth knowing

    *.vercel.app is a public suffix anyone can deploy to, and this location block sends
    Access-Control-Allow-Credentials: true. So any Vercel-hosted page can now make credentialed
    cross-origin calls to CKAN and read the responses with a visiting user's CKAN cookies attached.
    The pre-existing ~^https://.*\.github\.io$ entry has the same property, so this is a
    widening of an existing posture, not a new class of exposure — and CKAN here is a public
    catalog, which limits what it buys an attacker.

    ui-react does not need the credentialed path: ckanAction uses a plain fetch with no
    credentials: 'include', so it never sends cookies. If TACC wants this tightened, pinning the
    regex to the actual preview project (e.g. ~^https://ui-react-[a-z0-9-]+\.vercel\.app$) costs
    nothing here. Flagged, not blocking — TACC's infra, TACC's call.

  5. mosoriob commented on Aug 8, 2026

    @mosoriob
    ContributorAuthor

    Amendment 2 (2026-08-08): allowlist narrowed to the two real Vercel origins

    The broad ~^https://.*\.vercel\.app$ from Amendment 1 was replaced with anchored,
    exact-in-effect entries, and ~^https://.*\.github\.io$ was dropped at the same time. Live
    map $http_origin $cors_origin entries for this app:

    ~^https://monorepo-mosoriobs-projects\.vercel\.app$             $http_origin;
    ~^https://monorepo-git-develop-mosoriobs-projects\.vercel\.app$ $http_origin;

    Re-measured after reload — allowed: both Vercel origins, mint.tacc.utexas.edu,
    localhost:5173, 127.0.0.1:3000. Refused: *.github.io, bare *.vercel.app,
    evil.example.com, and http:// on the Vercel host. All four CKAN actions the app calls
    (package_search with and without ext_bbox, package_show, organization_list) return
    200 with the header from the branch alias, and the OPTIONS preflight echoes it.

    This closes the credentialed-access caveat from Amendment 1. No public-suffix wildcard
    remains in the allowlist, so the Access-Control-Allow-Credentials: true on that block is no
    longer reachable by arbitrary third-party pages.

    Constraint for #76

    Smoke-test against the branch alias, https://monorepo-git-develop-mosoriobs-projects.vercel.app.

    Per-deployment preview URLs (monorepo-<hash>-mosoriobs-projects.vercel.app) are not
    allowlisted and cannot be — the hash changes on every push. The branch alias points at the
    latest develop build, so it is the same artifact with a stable origin. A tester who grabs the
    per-deployment URL out of the Vercel dashboard will see every /datasets/* call fail as a
    fetch TypeError with no HTTP error; that is this allowlist, not a defect.

    Anyone testing a different branch needs its alias added to the map first — one line per
    branch, same shape.

    Note on the nginx config, for whoever touches it next

    Quoted (non-regex) map keys longer than ~64 bytes need map_hash_bucket_size 128;, and
    adding that to nginx.conf on this host fails with a spurious directive is duplicate error
    that we did not get to the bottom of — only one copy exists anywhere in the include closure,
    and 64 in the first error was just nginx's default, not a second setting. Anchored regex keys
    sidestep the hash table entirely, which is why the entries above are written as regexes rather
    than quoted strings. Use that form here.

    Unrelated but worth cleaning: /etc/nginx/conf.d/ holds six stale backups plus a
    letsencrypt-backup.tar.gz, saved from being parsed only by the *.conf glob on line 99 —
    while line 100 (include /etc/nginx/sites-enabled/*) has no extension filter at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

wayfinder:taskWayfinder ticket: manual work that unblocks a decision

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions