Skip to content
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

chore(indexer): security improvements #323

Merged
merged 1 commit into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions apps/indexer/src/routes/alchemy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import crypto from 'crypto'
import { addressActivityTask } from '@repo/trigger'
import type { Request, Response } from 'express'
import { appConfig } from '~/config'
import { logger } from '~/lib/logger'

/**
* Handles incoming Alchemy webhook requests.
* Validates the signature, logs the request, and triggers the address activity task.
* @param req - The incoming request object
* @param res - The response object
*/
export function alchemyWebhook(req: Request, res: Response) {
const evt = req.body as AlchemyWebhookEvent
logger.info(`Alchemy webhook received: ${evt.id}`)
if (!validateAlchemySignature(req)) return res.status(401).send('Unauthorized')
gaboesquivel marked this conversation as resolved.
Show resolved Hide resolved
logger.info('Validated Alchemy webhook 😀')
// TODO: validate user is whitelisted
gaboesquivel marked this conversation as resolved.
Show resolved Hide resolved

addressActivityTask.trigger(req.body)

res.status(200).send('Webhook processed')
}

/**
* Validates the Alchemy webhook signature.
* @param req - The incoming request object
* @returns boolean - True if the signature is valid, false otherwise
*/
function validateAlchemySignature(req: Request): boolean {
const alchemySignature = req.headers['x-alchemy-signature'] as string
const payload = JSON.stringify(req.body)
const hmac = crypto.createHmac(
'sha256',
appConfig.evm.alchemy.activitySigningKey,
gaboesquivel marked this conversation as resolved.
Show resolved Hide resolved
)
hmac.update(payload)
return alchemySignature === hmac.digest('hex')
gaboesquivel marked this conversation as resolved.
Show resolved Hide resolved
}



export interface AlchemyWebhookEvent {
webhookId: string;
id: string;
createdAt: Date;
type: AlchemyWebhookType;
event: Record<any, any>;
}

export type AlchemyWebhookType =
| "MINED_TRANSACTION"
| "DROPPED_TRANSACTION"
| "ADDRESS_ACTIVITY";
77 changes: 0 additions & 77 deletions apps/indexer/src/routes/alchemy/helpers.ts

This file was deleted.

31 changes: 0 additions & 31 deletions apps/indexer/src/routes/alchemy/index.ts

This file was deleted.

45 changes: 28 additions & 17 deletions apps/indexer/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import { logger } from '~/lib/logger'
import { setupSentryErrorHandler } from '~/lib/sentry'
import { alchemyWebhook } from './alchemy'
import { healthcheck } from './healthcheck'
import { addAlchemyContextToRequest, validateAlchemySignature } from './alchemy/helpers'
import { appConfig } from '~/config'

export function startExpress() {
const app = express()
Expand All @@ -20,30 +18,43 @@ export function startExpress() {
// Trust proxy
app.set('trust proxy', 1)

app.use(express.json())

// Sentry error handler
setupSentryErrorHandler(app)

// Security Middlewares
app.use(helmet())
app.use(express.json({ limit: '1mb' }))
gaboesquivel marked this conversation as resolved.
Show resolved Hide resolved

// Rate limiting
app.use(
rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // limit each IP to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
}),
)

//Logging middleware
app.use(pinoHttp({
logger,
serializers: {
req: (req) => JSON.stringify(req),
res: (res) => JSON.stringify(res),
err: (err) => JSON.stringify(err)
}
}))
// Sentry error handler
setupSentryErrorHandler(app)

// Logging middleware
// app.use(pinoHttp({
// logger,
// serializers: {
// req: (req) => JSON.stringify({
// method: req.method,
// url: req.url,
// headers: req.headers,
// }),
// res: (res) => JSON.stringify({
// statusCode: res.statusCode,
// }),
// err: (err) => JSON.stringify({
// type: err.constructor.name,
// message: err.message,
// stack: err.stack,
// }),
// },
// }))

// Routes
app.get('/', healthcheck)
app.post('/alchemy', alchemyWebhook)
Expand Down Expand Up @@ -81,4 +92,4 @@ export function startExpress() {
process.exit(0)
})
})
}
}
19 changes: 16 additions & 3 deletions apps/trigger/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { z } from 'zod'
import type { Address } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { isAddress } from 'viem'

const envSchema = z.object({
ISSUER_KEY: z.string().min(1).length(64).regex(/^[a-f0-9]+$/i, 'Invalid issuer key format'),
ISSUER_ADDRESS: z.string().refine((value): value is Address => isAddress(value), 'Invalid issuer address'),
})

const parsedEnv = envSchema.safeParse(process.env)
if (!parsedEnv.success) {
console.error(`Environment validation failed: ${JSON.stringify(parsedEnv.error.format())}`)
process.exit(1)
}

export const appenv = {
eosEvmApi: 'https://api.testnet.evm.eosnetwork.com',
issuerKey: process.env.ISSUER_KEY || '',
issuerAddress: (process.env.ISSUER_ADDRESS || '') as Address,
issuerAccount: privateKeyToAccount(`0x${process.env.ISSUER_KEY}`),
issuerKey: parsedEnv.data.ISSUER_KEY,
issuerAddress: parsedEnv.data.ISSUER_ADDRESS,
issuerAccount: privateKeyToAccount(`0x${parsedEnv.data.ISSUER_KEY}`),
}
3 changes: 2 additions & 1 deletion apps/trigger/src/trigger/activity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { logger, task } from '@trigger.dev/sdk/v3'
import { getErrorMessage } from 'app-lib'

// AlchemyWebhookEvent
export const addressActivityTask = task({
Expand All @@ -9,7 +10,7 @@ export const addressActivityTask = task({

} catch (error) {
logger.error('Error processing address activity', {
error: (error as Error).message,
error: getErrorMessage(error),
gaboesquivel marked this conversation as resolved.
Show resolved Hide resolved
})
throw error
}
Expand Down
Loading