Skip to content

Conversation

@paulhenri-l
Copy link
Contributor

@paulhenri-l paulhenri-l commented Nov 13, 2025

This PR adds flexibility to how and where workflows are queued and executed in world-postgres.

Queue Driver Abstraction

Previously, the queue implementation was hardcoded into the world. I've now extracted a QueueDriver interface that allows users to:

  • Use the default without any config (pg-boss)
  • Or Implement their custom queue drivers (useful if they already have a queue)

Proxy Strategies

Workflow code execution was done using the embedded world. I've added two execution strategies to handle this from within the world.

  • HTTP proxy: queue workers call /.well-known/workflow/v1/flow and /step endpoints
  • Function proxy: workers invoke directly the workflows/steps

Notes

  • To make the function proxy possible I had to add an export to the steps.js and workflows.js files
  • I need guidance on how it is expected to handle 503/Retry timeout. I think requeuing the job with proper delay is what's needed.
  • I may have missed other implementation details
  • I updated the README with some examples
  • The name "proxy" sounds a bit strange to me. In the end it's just a strategy pattern so maybe Strategy would be a better name

@changeset-bot
Copy link

changeset-bot bot commented Nov 13, 2025

🦋 Changeset detected

Latest commit: 376b4bd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@workflow/world-postgres Patch
@workflow/sveltekit Patch
@workflow/builders Patch
workflow Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/ai Patch
@workflow/world-testing Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Nov 13, 2025

@paulhenri-l is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@paulhenri-l paulhenri-l force-pushed the world-postgres branch 3 times, most recently from 239af80 to b35cfcb Compare November 17, 2025 22:54
@paulhenri-l paulhenri-l marked this pull request as ready for review November 17, 2025 23:29
@pranaygp
Copy link
Collaborator

pranaygp commented Nov 18, 2025

@paulhenri-l are you able to rebase on main by chance? I just added some postgres e2e tests in #356 that we should be able to run against the postgres queue driver changes

handler
) => {
return async (req) => {
const secret = req.headers.get('X-Workflow-Secret');
Copy link
Contributor

Choose a reason for hiding this comment

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

The request body is parsed before the security token is validated, creating a vulnerability where attackers can consume server resources by sending invalid payloads without authorization.

View Details
📝 Patch Details
diff --git a/packages/world-postgres/src/queue.ts b/packages/world-postgres/src/queue.ts
index 85d38ef..7734792 100644
--- a/packages/world-postgres/src/queue.ts
+++ b/packages/world-postgres/src/queue.ts
@@ -60,7 +60,6 @@ export function createQueue(
   ) => {
     return async (req) => {
       const secret = req.headers.get('X-Workflow-Secret');
-      const [message, payload] = await parse(req);
 
       if (!secret || securityToken !== secret) {
         return Response.json(
@@ -69,6 +68,8 @@ export function createQueue(
         );
       }
 
+      const [message, payload] = await parse(req);
+
       if (!isValidQueueName(message.queueName)) {
         return Response.json(
           { error: `Invalid queue name: ${message.queueName}` },

Analysis

Request body parsed before authorization check enables DoS vulnerability

What fails: The createQueueHandler function in packages/world-postgres/src/queue.ts parses and deserializes the request body before validating the security token, allowing attackers to consume server resources by sending invalid payloads without proper authorization.

How to reproduce:

# Send a request with an invalid or missing X-Workflow-Secret header
# The parse() function will still execute its expensive operations:
# - JSON parsing and schema validation
# - Base64 decoding  
# - JsonTransport deserialization
curl -X POST http://localhost:3000/queue \
  -H "Content-Type: application/json" \
  -d '{"queueName":"__wkf_workflow_test","data":"...","attempt":1,"messageId":"msg_123","id":"test"}'

Result: The request body is fully parsed despite failing authentication. An attacker can send many requests with large or complex payloads without valid credentials, consuming CPU and memory resources on the server.

Expected behavior: According to OWASP DoS Cheat Sheet, "using validation that is cheap in resources first" is a key mitigation. Authentication verification should occur before expensive operations like full body parsing and deserialization. Per security best practices, "access control should come before extensive validation" to prevent DoS attacks and resource exhaustion.

Impact:

  • DoS attacks: Attackers can exhaust server resources (CPU, memory) by sending large or complex payloads without valid credentials
  • Resource exhaustion: Each unauthorized request still processes CPU-intensive operations
  • Information disclosure: Different error responses during parsing vs. authorization could leak information

Fix applied: Moved the parse(req) call after the security token validation check, ensuring unauthorized requests fail fast without consuming parsing resources.

Comment on lines 40 to 48
import { createWorld, createPgBossQueue } from "@workflow/world-postgres";

const world = createWorld({
connectionString: "postgres://username:password@localhost:5432/database",
jobPrefix: "myapp", // optional
queueConcurrency: 10, // optional
securityToken: "your-secret-token-here",
queueFactory: createPgBossHttpProxyQueue({
jobPrefix: "my-app",
queueConcurrency: 10,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import { createWorld, createPgBossQueue } from "@workflow/world-postgres";
const world = createWorld({
connectionString: "postgres://username:password@localhost:5432/database",
jobPrefix: "myapp", // optional
queueConcurrency: 10, // optional
securityToken: "your-secret-token-here",
queueFactory: createPgBossHttpProxyQueue({
jobPrefix: "my-app",
queueConcurrency: 10,
})
import { createWorld, createPgBossHttpProxyQueue } from "@workflow/world-postgres";
const world = createWorld({
connectionString: "postgres://username:password@localhost:5432/database",
securityToken: "your-secret-token-here",
queueFactory: () => createPgBossHttpProxyQueue({
jobPrefix: "my-app",
queueConcurrency: 10,
})

The README code example for programmatic usage has an incorrect import (createPgBossQueue instead of createPgBossHttpProxyQueue) and passes the queue instance directly to queueFactory instead of wrapping it in a function, which violates the expected type signature () => QueueDriver.

View Details

Analysis

README contains incorrect code example for queueFactory configuration

What fails: The code example in packages/world-postgres/README.md lines 39-49 has two issues:

  1. Incorrect import statement: imports createPgBossQueue instead of createPgBossHttpProxyQueue
  2. Incorrect API usage: passes the result of createPgBossHttpProxyQueue({...}) directly to queueFactory instead of wrapping it in a function

How to reproduce: Copy the code example from packages/world-postgres/README.md lines 39-49 into a TypeScript project and attempt to use it:

  • TypeScript type checking will fail because queueFactory expects () => QueueDriver (a function), but receives QueueDriver (an instance)
  • The code will also fail at runtime when createWorld() attempts to invoke opts.queueFactory() on a non-function value

What happened vs expected:

  • Current (broken):

    import { createWorld, createPgBossQueue } from "@workflow/world-postgres";
    queueFactory: createPgBossHttpProxyQueue({...})
  • Expected:

    import { createWorld, createPgBossHttpProxyQueue } from "@workflow/world-postgres";
    queueFactory: () => createPgBossHttpProxyQueue({...})

The type definition in packages/world-postgres/src/config.ts shows queueFactory?: () => QueueDriver, and the implementation in packages/world-postgres/src/index.ts (line 18) calls opts.queueFactory() - confirming it must be a function. The correct pattern is demonstrated elsewhere in the same README at lines 118-122 and line 168.

Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
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