Skip to content

fix(api-service): add SSRF validation to HTTP step test endpoint fixes NV-7467#10959

Merged
scopsy merged 2 commits intonextfrom
cursor/ssrf-test-http-endpoint-nv-7467-79ca
May 3, 2026
Merged

fix(api-service): add SSRF validation to HTTP step test endpoint fixes NV-7467#10959
scopsy merged 2 commits intonextfrom
cursor/ssrf-test-http-endpoint-nv-7467-79ca

Conversation

@scopsy
Copy link
Copy Markdown
Contributor

@scopsy scopsy commented May 3, 2026

Summary

The workflow HTTP step test endpoint resolved Liquid templates into a URL and called HttpClientService.request() without the same SSRF checks as production (ExecuteHttpRequestStep).

Changes

  • Import validateUrlSsrf from @novu/application-generic.
  • After resolving URL/body/headers (and before secret key lookup and outbound request), call validateUrlSsrf(resolvedUrl).
  • On failure, return HTTP 400 with the validation error message in the response body, without performing the request. resolvedRequest includes the resolved body when present (same shape as success paths).
  • E2E: use a public HTTPS URL (https://httpbin.org/post) for happy-path body tests; add a regression test that http://localhost:... returns statusCode 400 with the SSRF error.

Testing

  • CI: E2E shard 3 previously failed because tests targeted localhost, which SSRF correctly blocks; e2e updated accordingly.

Linear Issue: NV-7467

Open in Web Open in Cursor 

What changed

The API's workflow HTTP step test endpoint now runs SSRF validation on the resolved URL (and related resolved body/headers) before decrypting secrets or making outbound requests. This closes a security gap where test requests previously bypassed production SSRF checks; on validation failure the endpoint returns HTTP 400 with the SSRF error and does not perform the outbound call.

Affected areas

api: TestHttpEndpointUsecase imports and calls validateUrlSsrf after Liquid resolution and before secret lookup/signature creation; on failure it returns a 400 response and includes the resolvedRequest payload (url, method, headers, and body when applicable). E2E tests were updated to use a public HTTPS endpoint for happy-path tests and a new regression test asserts localhost is rejected.

Key technical decisions

  • Reuse the existing validateUrlSsrf utility from @novu/application-generic to keep SSRF rules consistent with production ExecuteHttpRequestStep.
  • Run validation after template resolution but before secret decryption and the outbound HTTP request to avoid performing blocked network calls.
  • Validation failures return HTTP 400 with the validation message and a resolvedRequest shape matching success responses.

Testing

E2E updates added: happy-path tests now target https://httpbin.org/post and a regression test verifies localhost URLs return 400 with the SSRF error; CI shard failures caused by prior localhost tests were addressed. No new npm dependencies or enterprise changes were introduced.

Co-authored-by: Dima Grossman <dima@grossman.io>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 3, 2026

@netlify
Copy link
Copy Markdown

netlify Bot commented May 3, 2026

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit 2e77c27
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69f72bdb58bc49000872af27

@scopsy scopsy marked this pull request as ready for review May 3, 2026 10:55
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

TestHttpEndpointUsecase.execute now calls validateUrlSsrf(resolvedUrl) after resolving the request body/headers. If validation fails, the use case immediately returns a 400 payload with the SSRF error, empty headers, durationMs, and resolvedRequest; successful validation proceeds with secret decryption, signature creation, and the outbound request. Tests updated to use https://httpbin.org/post and a new SSRF rejection test was added.

Changes

SSRF Validation + Test updates

Layer / File(s) Summary
Dependency Import
apps/api/src/app/workflows-v2/usecases/test-http-endpoint/test-http-endpoint.usecase.ts
Import validateUrlSsrf from @novu/application-generic.
Validation Flow (short-circuit)
apps/api/src/app/workflows-v2/usecases/test-http-endpoint/test-http-endpoint.usecase.ts
Call validateUrlSsrf(resolvedUrl) after computing hasBody/resolved request. On error, return 400 with { error: ssrfValidationError }, empty headers, computed durationMs, and resolvedRequest (including body when applicable); subsequent secret decryption/signature steps are skipped.
Existing Flow (unchanged for success path)
apps/api/src/app/workflows-v2/usecases/test-http-endpoint/test-http-endpoint.usecase.ts
When validation passes, continue with secret decryption, novu-signature creation, outbound HTTP request, and existing HttpClientError mapping.
E2E Tests
apps/api/src/app/workflows-v2/e2e/test-http-endpoint.e2e.ts
Add HTTP_TEST_POST_URL = "https://httpbin.org/post", update two existing tests to use this URL, and add a new test asserting SSRF validation rejects localhost (expecting 400 and Requests to "localhost" are not allowed.).

Sequence Diagram

sequenceDiagram
  participant Client
  participant Usecase
  participant Validator
  participant Secrets
  participant HTTPClient
  participant ExternalAPI

  Client->>Usecase: request to test endpoint (resolvedUrl, method, body, headers)
  Usecase->>Validator: validateUrlSsrf(resolvedUrl)
  alt validation fails
    Validator-->>Usecase: error
    Usecase-->>Client: 400 { error, resolvedRequest, durationMs, headers: {} }
  else validation passes
    Validator-->>Usecase: ok
    Usecase->>Secrets: decrypt secret key
    Secrets-->>Usecase: decrypted key
    Usecase->>Usecase: build novu-signature header
    Usecase->>HTTPClient: send outbound HTTP request (with signature)
    HTTPClient->>ExternalAPI: outbound call
    ExternalAPI-->>HTTPClient: response
    HTTPClient-->>Usecase: response or HttpClientError
    Usecase-->>Client: formatted response
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title does not follow the Conventional Commits format correctly; 'api-service' is not a valid scope and the Linear ticket reference is not properly positioned. Change title to: 'fix(api): add SSRF validation to HTTP step test endpoint fixes NV-7467' to use valid scope 'api' and proper formatting.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-authored-by: Dima Grossman <dima@grossman.io>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/workflows-v2/e2e/test-http-endpoint.e2e.ts`:
- Around line 18-27: The test currently builds the SSRF URL using
process.env.PORT which can be undefined; change the test to derive the port from
the running test server instead of process.env.PORT (e.g. replace the literal
URL construction that uses `process.env.PORT` with one built from the test
server instance like `server.address().port` or use the test framework's helper
that returns the base URL), so the GET to `/v1/health-check` always targets the
actual listening port; update the URL string in the request invocation (the code
that constructs `http://localhost:${process.env.PORT}/v1/health-check`) to use
the server-derived port and keep the existing `response` assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a557e7b7-59a0-42ea-89ce-5661b95ad055

📥 Commits

Reviewing files that changed from the base of the PR and between df40814 and 2e77c27.

📒 Files selected for processing (2)
  • apps/api/src/app/workflows-v2/e2e/test-http-endpoint.e2e.ts
  • apps/api/src/app/workflows-v2/usecases/test-http-endpoint/test-http-endpoint.usecase.ts

Comment on lines 18 to +27
url: `http://localhost:${process.env.PORT}/v1/health-check`,
method: 'GET',
},
});

expect(response.status).to.equal(201);
expect(response.body.data.statusCode).to.equal(400);
expect(response.body.data.body).to.deep.include({
error: 'Requests to "localhost" are not allowed.',
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the SSRF regression test independent of process.env.PORT.

If process.env.PORT is unset, this URL can become invalid and fail with a different error than the intended localhost SSRF rejection.

Proposed fix
-        url: `http://localhost:${process.env.PORT}/v1/health-check`,
+        url: 'http://localhost/v1/health-check',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
url: `http://localhost:${process.env.PORT}/v1/health-check`,
method: 'GET',
},
});
expect(response.status).to.equal(201);
expect(response.body.data.statusCode).to.equal(400);
expect(response.body.data.body).to.deep.include({
error: 'Requests to "localhost" are not allowed.',
});
url: 'http://localhost/v1/health-check',
method: 'GET',
},
});
expect(response.status).to.equal(201);
expect(response.body.data.statusCode).to.equal(400);
expect(response.body.data.body).to.deep.include({
error: 'Requests to "localhost" are not allowed.',
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/workflows-v2/e2e/test-http-endpoint.e2e.ts` around lines 18
- 27, The test currently builds the SSRF URL using process.env.PORT which can be
undefined; change the test to derive the port from the running test server
instead of process.env.PORT (e.g. replace the literal URL construction that uses
`process.env.PORT` with one built from the test server instance like
`server.address().port` or use the test framework's helper that returns the base
URL), so the GET to `/v1/health-check` always targets the actual listening port;
update the URL string in the request invocation (the code that constructs
`http://localhost:${process.env.PORT}/v1/health-check`) to use the
server-derived port and keep the existing `response` assertions unchanged.

@scopsy scopsy merged commit 20ffd7c into next May 3, 2026
33 checks passed
@scopsy scopsy deleted the cursor/ssrf-test-http-endpoint-nv-7467-79ca branch May 3, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants