Skip to content

Conversation

@Trynax
Copy link
Contributor

@Trynax Trynax commented Nov 8, 2025

Close #612

When a user provides a template URL (e.g., https://github.com/trynax/template), the system:

  • Extracts the GitHub username from the URL
  • Looks up the Echo user with that GitHub account
  • Creates/retrieves their referral code
  • Sets them as the referrer for the new app

@vercel
Copy link
Contributor

vercel bot commented Nov 8, 2025

@Trynax is attempting to deploy a commit to the Merit Systems Team on Vercel.

A member of the Team first needs to authorize it.

Comment on lines 9 to 38
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validatedData = registerTemplateReferralSchema.parse(body);
const { appId, githubUsername, templateUrl } = validatedData;

const app = await db.echoApp.findUnique({
where: { id: appId },
include: {
appMemberships: {
where: { role: AppRole.OWNER },
take: 1,
},
},
});

if (!app || app.appMemberships.length === 0) {
return NextResponse.json(
{ success: false, message: 'App not found' },
{ status: 404 }
);
}

const userId = app.appMemberships[0].userId;

const result = await registerTemplateReferral(userId, {
appId,
githubUsername,
templateUrl,
});
Copy link
Contributor

@vercel vercel bot Nov 8, 2025

Choose a reason for hiding this comment

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

The /api/v1/referrals/register-template endpoint lacks authentication and accepts any appId, allowing anyone to set arbitrary referrers for any app in the system. This is a critical authorization bypass.

View Details
📝 Patch Details
diff --git a/packages/app/control/src/app/api/v1/referrals/register-template/route.ts b/packages/app/control/src/app/api/v1/referrals/register-template/route.ts
index 5a855111..51b14eba 100644
--- a/packages/app/control/src/app/api/v1/referrals/register-template/route.ts
+++ b/packages/app/control/src/app/api/v1/referrals/register-template/route.ts
@@ -1,76 +1,65 @@
-import { NextResponse, NextRequest } from 'next/server';
+import { NextResponse } from 'next/server';
 import {
   registerTemplateReferral,
   registerTemplateReferralSchema,
 } from '@/services/db/apps/template-referral';
-import { db } from '@/services/db/client';
-import { AppRole } from '@/services/db/apps/permissions/types';
+import { authRoute } from '../../../../../lib/api/auth-route';
 
-export async function POST(request: NextRequest) {
-  try {
-    const body = await request.json();
-    const validatedData = registerTemplateReferralSchema.parse(body);
-    const { appId, githubUsername, templateUrl } = validatedData;
+export const POST = authRoute
+  .body(registerTemplateReferralSchema)
+  .handler(async (_, context) => {
+    try {
+      const { appId, githubUsername, templateUrl } = context.body;
+      const { userId, appId: contextAppId } = context.ctx;
 
-    const app = await db.echoApp.findUnique({
-      where: { id: appId },
-      include: {
-        appMemberships: {
-          where: { role: AppRole.OWNER },
-          take: 1,
-        },
-      },
-    });
-
-    if (!app || app.appMemberships.length === 0) {
-      return NextResponse.json(
-        { success: false, message: 'App not found' },
-        { status: 404 }
-      );
-    }
+      // Verify that the authenticated user owns the app they're trying to modify
+      if (contextAppId !== appId) {
+        return NextResponse.json(
+          { success: false, message: 'Access denied: You can only modify referrals for your own apps' },
+          { status: 403 }
+        );
+      }
 
-    const userId = app.appMemberships[0].userId;
+      const result = await registerTemplateReferral(userId, {
+        appId,
+        githubUsername,
+        templateUrl,
+      });
 
-    const result = await registerTemplateReferral(userId, {
-      appId,
-      githubUsername,
-      templateUrl,
-    });
+      if (result.status === 'registered') {
+        return NextResponse.json({
+          success: true,
+          status: 'registered',
+          message: `Template creator ${result.referrerUsername} registered as referrer`,
+          referrerUsername: result.referrerUsername,
+        });
+      }
 
-    if (result.status === 'registered') {
-      return NextResponse.json({
-        success: true,
-        status: 'registered',
-        message: `Template creator ${result.referrerUsername} registered as referrer`,
-        referrerUsername: result.referrerUsername,
-      });
-    }
+      if (result.status === 'skipped') {
+        return NextResponse.json({
+          success: true,
+          status: 'skipped',
+          message: result.reason || 'Referral skipped',
+          reason: result.reason,
+        });
+      }
 
-    if (result.status === 'skipped') {
       return NextResponse.json({
         success: true,
-        status: 'skipped',
-        message: result.reason || 'Referral skipped',
+        status: 'not_found',
+        message: result.reason || 'Template creator not found on Echo',
         reason: result.reason,
       });
-    }
-
-    return NextResponse.json({
-      success: true,
-      status: 'not_found',
-      message: result.reason || 'Template creator not found on Echo',
-      reason: result.reason,
-    });
-  } catch (error) {
-    const message =
-      error instanceof Error ? error.message : 'Unknown error occurred';
+    } catch (error) {
+      const message =
+        error instanceof Error ? error.message : 'Unknown error occurred';
 
-    return NextResponse.json(
-      {
-        success: false,
-        message,
-      },
-      { status: 400 }
-    );
-  }
-}
\ No newline at end of file
+      return NextResponse.json(
+        {
+          success: false,
+          message,
+        },
+        { status: 400 }
+      );
+    }
+  });
\ No newline at end of file

Analysis

Unauthenticated API endpoint allows unauthorized referral modification

What fails: /api/v1/referrals/register-template endpoint accepts any appId without authentication, allowing attackers to set themselves as referrers for arbitrary apps

How to reproduce:

# Call endpoint with any app ID and attacker's GitHub username:
curl -X POST https://echo.merit.systems/api/v1/referrals/register-template \
  -H "Content-Type: application/json" \
  -d '{"appId":"<any-app-id>","githubUsername":"attacker","templateUrl":"https://github.com/attacker/repo"}'

Result: Returns 200 OK and sets attacker as referrer for the target app's owner without permission checks. The endpoint looks up the app owner from the database and modifies their referral data directly.

Expected: Should return 401/403 Unauthorized. API endpoints that modify user data require authentication via authRoute middleware (like /api/v1/user/referral/route.ts)

Impact: Authorization bypass allows hijacking referral earnings by setting arbitrary referrers for any app in the system

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yoo @rsproule the authentication concern on this /api/v1/referrals/register-template, I believe this isn't significant because app builders typically won't reveal their app ID publicly before scaffolding with echo-start, and once a referrer is set, subsequent calls are automatically skipped so it can't be overridden by echo start.

@rsproule
Copy link
Contributor

some problems:

  • set template referrer should be authed so that only the app owner should be able to set this
  • the github linking here is wrong. that is for payouts, the only thing we need is the authed user id of the app owner
  • we point to the referer via a referral code that they should have defined somewhere in their template
  • the strategy of pulling it off of the repo username is not going to work whenever the template over has not connected their github. this should not be a requirement. we just need their echo ref code

@Trynax
Copy link
Contributor Author

Trynax commented Nov 11, 2025

some problems:

I've worked on them.

  • set template referrer should be authed so that only the app owner should be able to set this
  • the github linking here is wrong. that is for payouts, the only thing we need is the authed user id of the app owner
  • we point to the referer via a referral code that they should have defined somewhere in their template
  • the strategy of pulling it off of the repo username is not going to work whenever the template over has not connected their github. this should not be a requirement. we just need their echo ref code
  • User has to authenticate using their API key now. Not sure if this is the best UX, but it ensures only the app owner can set the referrer.
  • Removed the GitHub linking completely.
  • Template creators can include their referral code in echo.config.json, We can change the format/location if needed. Also realized there's no endpoint yet to fetch my own referral code from echo, I had to seed the local db to get one for testing.

example of echo.config.json:

{
  "referralCode": "4f9cc5ee-f3fd-4f60-bebf-dc2f6148c35a"
}

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.

Template referral system

2 participants