Skip to content

Add RDI native API proxy endpoint for @rdi-ui/pipeline - #6504

Merged
ArtemHoruzhenko merged 3 commits into
mainfrom
feature/rdi-ui/proxy-endpoint-v3
Sep 15, 2026
Merged

ArtemHoruzhenko merged 3 commits into
mainfrom
feature/rdi-ui/proxy-endpoint-v3

Conversation

@ArtemHoruzhenko

@ArtemHoruzhenko ArtemHoruzhenko commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

What

@rdi-ui/pipeline ships its own SDK that speaks the native RDI API. This routes those calls through RedisInsight's own API so instance credentials and self-signed-certificate handling stay server-side and the browser never talks to RDI directly — which also takes CORS out of the picture.

Replaces #6503 (same goal, reworked from scratch off latest main).

The change in approach is the point. #6503 aimed at a transparent proxy, and that scope is what generated the review load — 23 findings across it and #6468, of which 20 fell into three buckets: redirect Location handling (11), raw-body/payload limits (6), and header denylist misses (3). All three are consequences of transparency, not individual defects.

This is deliberately a narrow JSON passthrough for one known client instead:

  • Headers are allowlisted in both directions. RedisInsight's own credentials (x-csrf-token, x-window-id) can't leak upstream, and RDI can't set origin-scoped policy (HSTS, CSP, report-to, nel) on RedisInsight's origin. A denylist needs extending every time either side adds a header.
  • Upstream redirects are refused with a 502, not followed or rewritten. Following one makes our backend issue the follow-up request with the RDI bearer token attached; rewriting Location back onto the proxy means reconstructing the global prefix and RI_PROXY_PATH. The RDI API has exactly one redirect (GET //docs) and the SDK never requests it. This also clears the CodeQL server-side-redirect alert from Add transparent proxy endpoint for RDI native API #6468.
  • The upstream URL is resolved once, so the URL that is validated is the URL that is sent — no decoded-vs-encoded divergence, which was the root cause behind the encoded-delimiter and dot-segment findings. Containment is checked against rdi.url's path, so an RDI hosted under a subpath stays scoped and an absolute or scheme-relative path is rejected.
  • Non-JSON content types are refused with a 415. Every RDI endpoint is JSON in and JSON out (verified against the RDI source: no multipart, UploadFile, octet-stream, PlainTextResponse, media_type= or response_class anywhere), and the globally installed JSON body parser already enforces maxPayloadSize and its 413 handling — so main.ts is untouched.

Net effect: ~197 lines of source vs ~297 in #6503, and the two unbounded finding-generators are gone by construction rather than fixed case by case.

Structure follows the module: RdiProxyController (HTTP adapter) → RdiProxyService (header/redirect policy) → ApiRdiClient.proxyRequest (transport, reusing the client's authenticated axios instance). URL handling lives in utils/rdi-proxy.util.ts as pure functions. proxyRequest is not added to the abstract RdiClientApiV2RdiClient extends ApiRdiClient, so both concrete clients get it while the abstraction stays clean.

Testing

  • npx jest -w 1 src/modules/rdi — 367 tests / 29 suites passing
  • npm run type-check — baseline gate green, "no new errors"
  • eslint + prettier --check on all touched files — clean
  • Nest routing verified with supertest: req.url preserves %2F through routing (the assumption behind getRdiUpstreamPath), and the bare rdi/:id/proxy is a 404 by design
  • resolveRdiUpstreamUrl has 31 focused tests covering absolute-URL rejection, scheme-relative paths, ../ / %2e%2e / .%2e escapes, encoded-slash containment, trailing-slash normalisation, and the sibling-prefix case (/rdi-other vs base /rdi/)

Manual verification against a live RDI instance has not been done yet — the checks above are automated only.

Known gaps

  • No test/api/ integration test, though the convention exists (test/api/rdi/GET-rdi-id-*.test.ts). Happy to add one if wanted.
  • No analytics events. Other RDI services emit via RdiAnalytics; a per-request proxy event looked like noise, but say the word.

🤖 Generated with Claude Code


Note

High Risk
New server-side proxy to user-configured RDI hosts with bearer-token upstream calls; mitigations (URL containment, header allowlists, no redirects) are central to avoiding SSRF and credential/header leakage.

Overview
Adds a narrow JSON passthrough at rdi/:id/proxy/* so the @rdi-ui/pipeline SDK can call the RDI native API through RedisInsight (credentials, TLS, and CORS stay server-side).

HTTP surface: RdiProxyController allows GET/POST/PUT/PATCH/DELETE, rejects non-application/json bodies with 415 (avoids forwarding empty bodies when the global parser did not run), and streams upstream status/headers/raw body back to the client.

Policy layer: RdiProxyService forwards only allowlisted request headers (content-type, accept) and response headers (content-type), injects content-security-policy: sandbox and x-content-type-options: nosniff, and turns upstream redirect statuses (301/302/303/307/308) into 502 instead of following or exposing Location.

Transport: ApiRdiClient.proxyRequest resolves the target URL, reuses the authenticated axios client with validateStatus: null, maxRedirects: 0, and responseType: 'arraybuffer' so non-2xx and odd JSON shapes pass through unchanged.

URL safety: rdi-proxy.util strips the proxy mount from the raw URL (preserving percent-encoding), builds the upstream URL once, enforces containment under the configured rdi.url, and rejects traversal/double-encoding via segment inertness checks.

Wired into RdiModule with new RdiProxyRequest/RdiProxyResponse types and unit tests across controller, service, client, and URL helpers.

Reviewed by Cursor Bugbot for commit 1b525e6. Bugbot is set up for automated code reviews on this repo. Configure here.

@rdi-ui/pipeline ships its own SDK that speaks the native RDI API. Route
those calls through RedisInsight so instance credentials and
self-signed-certificate handling stay server-side and the browser never
talks to RDI directly - which also takes CORS out of the picture.

Deliberately scoped to a narrow JSON passthrough for that one client
rather than a general-purpose proxy, since "transparent" is what makes
this kind of endpoint hard to get right:

- Headers are allowlisted in both directions. RedisInsight's own
  credentials (x-csrf-token, x-window-id) cannot leak upstream, and RDI
  cannot set origin-scoped policy (HSTS, CSP, report-to, nel) on
  RedisInsight's origin. A denylist would need extending every time
  either side adds a header.
- Upstream redirects are refused with a 502 rather than followed or
  rewritten. Following one would make our backend issue the follow-up
  request with the RDI bearer token attached; rewriting Location back
  onto the proxy means reconstructing the global prefix and
  RI_PROXY_PATH. The RDI API has exactly one redirect (GET / -> /docs)
  and the SDK never requests it.
- The upstream URL is resolved once, so the URL that gets validated is
  the URL that gets sent - no decoded-vs-encoded divergence. Containment
  is checked against rdi.url's path, so an RDI hosted under a subpath
  stays scoped and an absolute or scheme-relative path is rejected.
- Non-JSON content types are refused with a 415. Every RDI endpoint is
  JSON, and the globally installed JSON body parser already enforces
  maxPayloadSize and its 413 handling.

Responses are read as arraybuffer so axios never parses and
re-serializes a payload, and carry a forced sandboxed CSP and nosniff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ArtemHoruzhenko
ArtemHoruzhenko requested a review from a team as a code owner September 14, 2026 13:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6925180165

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread redisinsight/api/src/modules/rdi/utils/rdi-proxy.util.ts
Comment thread redisinsight/api/src/modules/rdi/rdi-proxy.controller.ts Outdated
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Code Coverage - Backend unit tests

St.
Category Percentage Covered / Total
🟢 Statements 93.29% 16884/18099
🟡 Branches 75.68% 5429/7174
🟢 Functions 87.75% 2592/2954
🟢 Lines 93.15% 16146/17334

Test suite run success

3924 tests passing in 331 suites.

Report generated by 🧪jest coverage report action from 1b525e6

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Code Coverage - Integration Tests

Status Category Percentage Covered / Total
🟡 Statements 79.6% 18582/23344
🟡 Branches 62.05% 8693/14009
🟡 Functions 67.34% 2516/3736
🟡 Lines 79.19% 17496/22092

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6925180. Configure here.

Comment thread redisinsight/api/src/modules/rdi/rdi-proxy.controller.ts
…containment

Both from PR review on #6504.

The content-type guard accepted `+json` suffixes
(`application/merge-patch+json`, `application/problem+json`), but
`bodyParser.json()`'s default type is the bare `application/json` and
skips those - so `req.body` arrived undefined and the request was
forwarded with an empty body and the caller's content type. That is the
exact failure the guard was added to prevent. Narrowed the pattern to
what the parser actually parses, verified case-by-case against `type-is`
with body-parser's own type string, and documented that widening one
without the other reintroduces the bug. RDI's API uses no `+json` type.

Path containment only checked the URL as sent. `new URL()` correctly
treats %2F as an opaque character rather than a separator, so
`..%2fadmin` passed as a single segment - but an upstream (or a reverse
proxy in front of RDI) that percent-decodes before normalizing dot
segments reads the same path as `../admin` and lands outside the
configured subpath, with the RDI bearer token attached. Now validates
that reading as well, so containment holds whichever order the upstream
uses. An encoded slash inside a real path segment still passes, since it
resolves within the base either way - RDI's name path params are
unconstrained strings, so banning %2F outright would have been wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc771aa474

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread redisinsight/api/src/modules/rdi/utils/rdi-proxy.util.ts Outdated
…encodings

Review on #6504 reported `%252e%252e%252fadmin`: the previous fix only
recognised direct `%2f`/`%5c`, so a nested encoding passed both
containment checks and two decoding layers in front of RDI would
normalize it to `/admin`.

That is the fourth variant of one attack (`..`, then `%2e%2e`, then
`..%2f`, now `%252e%252e%252f`), which is the real signal: adding a
`%25` case would just invite `%2525`. Predicting how many layers decode,
and in what order, is not a winnable game - there is always one more
encoding, including overlong UTF-8 and invalid escapes.

So this stops predicting. Every caller-supplied path segment must now be
inert: it has to decode cleanly, and the result must not contain `/`,
`\`, `?`, `#` or `%`, nor be a dot segment. `%` is the load-bearing
case - encoding a `%` is the only way to nest encodings at all, so
refusing it rules out every depth with one condition. This replaces the
single-pass separator check rather than adding to it.

Trade-off, taken deliberately: a pipeline name containing `/` or `%` can
no longer be addressed through the proxy. This reverses the reasoning in
my earlier review reply, which defended allowing an encoded separator
inside a segment - that is precisely what keeps the decoding-layer
question open. Names are identifier-shaped in practice and an encoded
separator would not survive RDI's own path routing either. Spaces and
non-ASCII characters are unaffected and covered by tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ArtemHoruzhenko
ArtemHoruzhenko merged commit 8eed8d7 into main Sep 15, 2026
30 checks passed
@ArtemHoruzhenko
ArtemHoruzhenko deleted the feature/rdi-ui/proxy-endpoint-v3 branch September 15, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants