Skip to content
Open
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
21 changes: 21 additions & 0 deletions SECURITY_AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Security Audit Findings

## Critical Issues

*None identified at this time.*

## Low/Medium Issues

### 1. `alert_admin` DB Session Handling
**Severity:** Low
**Description:** The `alert_admin` tool manually manages a database session obtained from `get_db_session` generator. While it correctly closes it, the generator remains suspended until garbage collection.
**Recommendation:** Ensure `get_db_session` is used as a context manager if possible, or refactor tool to use `scoped_session` similar to `agent_stream_endpoint`.

### 2. Missing Rate Limiting on Webhooks
**Severity:** Low
**Description:** While signature verification prevents unauthorized payloads, the webhook endpoint is still exposed to DoS attacks.
**Recommendation:** Implement rate limiting (e.g., via Nginx or application middleware) for the webhook endpoint.

### 3. Usage Reset Logic
**Severity:** Low
**Description:** The `handle_usage_reset_webhook` trusts `invoice.payment_succeeded` events to reset usage. Ensure idempotency keys are handled to prevent double-processing if Stripe retries webhooks (though resetting to 0 is idempotent-ish).
27 changes: 16 additions & 11 deletions alembic/versions/33ae457b2ddf_add_referral_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Create Date: 2025-12-26 10:37:46.325765

"""

from typing import Sequence, Union

from alembic import op
Expand All @@ -13,26 +14,28 @@
from sqlalchemy.ext.declarative import declarative_base

# revision identifiers, used by Alembic.
revision: str = '33ae457b2ddf'
down_revision: Union[str, Sequence[str], None] = '8b9c2e1f4c1c'
revision: str = "33ae457b2ddf"
down_revision: Union[str, Sequence[str], None] = "8b9c2e1f4c1c"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

# Define a minimal model for data migration
Base = declarative_base()


class Profile(Base):
__tablename__ = 'profiles'
__tablename__ = "profiles"
user_id = sa.Column(sa.UUID, primary_key=True)
referral_code = sa.Column(sa.String)
referral_count = sa.Column(sa.Integer)


def upgrade() -> None:
"""Upgrade schema."""
# 1. Add columns as nullable first
op.add_column('profiles', sa.Column('referral_code', sa.String(), nullable=True))
op.add_column('profiles', sa.Column('referrer_id', sa.UUID(), nullable=True))
op.add_column('profiles', sa.Column('referral_count', sa.Integer(), nullable=True))
op.add_column("profiles", sa.Column("referral_code", sa.String(), nullable=True))
op.add_column("profiles", sa.Column("referrer_id", sa.UUID(), nullable=True))
op.add_column("profiles", sa.Column("referral_count", sa.Integer(), nullable=True))

# 2. Backfill existing rows with 0 count
bind = op.get_bind()
Expand All @@ -45,10 +48,12 @@ def upgrade() -> None:
# 3. Alter columns
# referral_code stays nullable=True
# referral_count becomes nullable=False
op.alter_column('profiles', 'referral_count', nullable=False)
op.alter_column("profiles", "referral_count", nullable=False)

# 4. Create unique constraint and index
op.create_unique_constraint("uq_profiles_referral_code", "profiles", ["referral_code"])
op.create_unique_constraint(
"uq_profiles_referral_code", "profiles", ["referral_code"]
)
op.create_index("ix_profiles_referral_code", "profiles", ["referral_code"])

# Add foreign key for referrer_id
Expand All @@ -62,6 +67,6 @@ def downgrade() -> None:
op.drop_constraint("fk_profiles_referrer_id", "profiles", type_="foreignkey")
op.drop_index("ix_profiles_referral_code", table_name="profiles")
op.drop_constraint("uq_profiles_referral_code", "profiles", type_="unique")
op.drop_column('profiles', 'referral_count')
op.drop_column('profiles', 'referrer_id')
op.drop_column('profiles', 'referral_code')
op.drop_column("profiles", "referral_count")
op.drop_column("profiles", "referrer_id")
op.drop_column("profiles", "referral_code")
31 changes: 17 additions & 14 deletions src/api/routes/payments/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,23 @@ def _try_construct_event(payload: bytes, sig_header: str | None) -> dict:
"""

def _secrets() -> Iterable[str]:
primary = (
global_config.STRIPE_WEBHOOK_SECRET
if global_config.DEV_ENV == "prod"
else global_config.STRIPE_TEST_WEBHOOK_SECRET
)
secondary = (
global_config.STRIPE_TEST_WEBHOOK_SECRET
if global_config.DEV_ENV == "prod"
else global_config.STRIPE_WEBHOOK_SECRET
)
if primary:
yield primary
if secondary and secondary != primary:
yield secondary
# STRICTLY enforce secret usage based on environment
# Prod env MUST use Prod secret.
# Non-prod env MUST use Test secret.
# No fallbacks across environments.

if global_config.DEV_ENV == "prod":
if global_config.STRIPE_WEBHOOK_SECRET:
yield global_config.STRIPE_WEBHOOK_SECRET
else:
logger.error("STRIPE_WEBHOOK_SECRET not configured in PROD")
else:
if global_config.STRIPE_TEST_WEBHOOK_SECRET:
yield global_config.STRIPE_TEST_WEBHOOK_SECRET
else:
logger.warning(
f"STRIPE_TEST_WEBHOOK_SECRET not configured in {global_config.DEV_ENV}"
)

if not sig_header:
raise HTTPException(status_code=400, detail="Missing stripe-signature header")
Expand Down
5 changes: 3 additions & 2 deletions src/db/utils/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import uuid
from loguru import logger


def ensure_profile_exists(
db: Session,
user_uuid: uuid.UUID,
email: str | None = None,
username: str | None = None,
avatar_url: str | None = None,
is_approved: bool = False
is_approved: bool = False,
) -> Profiles:
"""
Ensure a profile exists for the given user UUID.
Expand All @@ -27,7 +28,7 @@ def ensure_profile_exists(
email=email,
username=username,
avatar_url=avatar_url,
is_approved=is_approved
is_approved=is_approved,
)
db.add(profile)
# No need for explicit commit/refresh as db_transaction handles commit,
Expand Down
Loading