-
Notifications
You must be signed in to change notification settings - Fork 105
World postgres - queue driver #332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 376b4bd The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
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 |
|
@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. |
239af80 to
b35cfcb
Compare
|
@paulhenri-l are you able to rebase on |
091a599 to
d3a555e
Compare
| handler | ||
| ) => { | ||
| return async (req) => { | ||
| const secret = req.headers.get('X-Workflow-Secret'); |
There was a problem hiding this comment.
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.
b231836 to
b1b725a
Compare
packages/world-postgres/README.md
Outdated
| 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, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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:
- Incorrect import statement: imports
createPgBossQueueinstead ofcreatePgBossHttpProxyQueue - Incorrect API usage: passes the result of
createPgBossHttpProxyQueue({...})directly toqueueFactoryinstead 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
queueFactoryexpects() => QueueDriver(a function), but receivesQueueDriver(an instance) - The code will also fail at runtime when
createWorld()attempts to invokeopts.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]>
Signed-off-by: paulhenri-l <[email protected]>
Signed-off-by: paulhenri-l <[email protected]>
d7f9ee6 to
376b4bd
Compare
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:
Proxy Strategies
Workflow code execution was done using the embedded world. I've added two execution strategies to handle this from within the world.
Notes