Skip to content

Instantly share code, notes, and snippets.

@jaonoctus
Last active March 22, 2026 00:33
Show Gist options
  • Select an option

  • Save jaonoctus/421fff0dc0d4073b0c70d5a78ffff5a7 to your computer and use it in GitHub Desktop.

Select an option

Save jaonoctus/421fff0dc0d4073b0c70d5a78ffff5a7 to your computer and use it in GitHub Desktop.
[PATCHED] Vulnerability Report — BRIX

Vulnerability Report — BRIX (Lightning Address Router)

I identified seven security vulnerabilities in the BRIX server. I am committed to working with the BRIX maintainers to resolve these issues responsibly.

Summary

Three vulnerabilities are externally exploitable with no authentication required and have been confirmed against the live instance at https://brix.brostr.app. One of these (VULN-7) is a full account takeover chain rated Critical (CVSS 10.0). The remaining four require internal access (database or deployment environment) but represent significant weaknesses in the PII protection model.

No data was exfiltrated. A test account (brixtest001@brix.brostr.app) was created solely to demonstrate the unauthenticated API findings and will not be used further.

Product

BRIX — Lightning Address Router

Tested Instance

https://brix.brostr.app — live production, tested 2026-03-20.

Findings Overview

ID Title Severity CVSS Vector
VULN-1 Plaintext PII in brix_verifications.destination Medium 4.4 Internal
VULN-2 PII and OTP codes leaked in server logs Medium 4.9 Internal
VULN-3 Missing encryption key silently disables PII protection Medium 6.0 Internal
VULN-4 HMAC-SHA256 brute-forceable for structured PII Medium 5.9 Internal
VULN-5 Unauthenticated PII disclosure via /brix/find-by-email High 7.5 External
VULN-6 Unauthenticated phone → Nostr pubkey correlation via /brix/resolve High 7.5 External
VULN-7 Full account takeover via unauthenticated update-contact Critical 10.0 External

VULN-1: Plaintext PII in brix_verifications.destination

Severity: Medium — CVSS v3.1: 4.4 (AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:N/A:N)

Description

While brix_users.phone and brix_users.email are encrypted at rest via AES-256-GCM, the brix_verifications table is not. Every OTP flow (register, resend, update-contact) writes the raw phone number or email to the destination column:

INSERT INTO brix_verifications (id, user_id, code, type, destination, expires_at)

These rows persist in the database indefinitely.

Impact

A database dump or SQL injection would expose all phone numbers and emails that ever went through verification — effectively bypassing the encryption applied to brix_users.

Remediation

Encrypt destination with the same AES-256-GCM scheme before writing. Decrypt only when needed for resend or display.

References


VULN-2: PII and OTP Codes Leaked in Server Logs

Severity: Medium — CVSS v3.1: 4.9 (AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N)

Description

Multiple console.log statements emit plaintext phone numbers, email addresses, and active OTP codes to stdout:

console.log(`[BRIX] Código de verificação para ${destination}: ${code}`);
console.log(`[BRIX] Novo código para ${destination}: ${code}`);
console.log(`[BRIX] Update-contact code for ${destination}: ${code}`);

Impact

Anyone with log access (operators, compromised log aggregation pipelines, cloud provider staff) can harvest all phone numbers and emails processed by the system and read live OTP codes to complete verification flows — including account takeover via update-contact (see VULN-7).

Remediation

Mask or omit destination from logs — log only the verification type and user ID. Never log OTP codes under any circumstance.

References


VULN-3: Missing Encryption Key Silently Disables All PII Protection

Severity: Medium — CVSS v3.1: 6.0 (AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:N)

Description

Both encrypt() and hmacHash() contain a silent passthrough when BRIX_ENCRYPTION_KEY is not set:

if (!key) return plaintext; // passthrough if no key configured

A deployment without this environment variable stores all PII unencrypted while the application behaves normally — a classic fail-open pattern. There is no startup error, no warning, and no way to detect the condition from the stored data.

Impact

The entire PII protection layer is silently disabled by a single missing environment variable. Data stored during this window is permanently unprotected.

Remediation

Throw a fatal error at startup when BRIX_ENCRYPTION_KEY is absent and PII features are in use. At minimum, emit a loud warning on every call to encrypt() or hmacHash() when the key is missing.

References


VULN-4: HMAC-SHA256 Brute-Forceable for Structured PII

Severity: Medium — CVSS v3.1: 5.9 (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N)

Description

phone_hash and email_hash are computed with HMAC-SHA256:

crypto.createHmac('sha256', key).update(value).digest('hex')

HMAC-SHA256 is designed for message authentication, not for hashing low-entropy secrets. It can be evaluated billions of times per second on commodity hardware. Phone numbers (structured, small keyspace) and emails (targetable by known domains) can be reversed exhaustively in seconds.

The preferred primitive is Argon2id (RFC 9106), winner of the Password Hashing Competition — intentionally slow and memory-hard, making offline brute-force infeasible even with full key and database access.

Impact

An attacker who compromises the key and the database can reverse every phone and email hash in the system in seconds.

Remediation

Replace hmacHash() with Argon2id (e.g. via the argon2 npm package). Use a fixed per-deployment salt derived from BRIX_ENCRYPTION_KEY so lookups remain deterministic while brute-force cost stays high.

References


VULN-5: Unauthenticated PII Disclosure via /brix/find-by-email

Severity: High — CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Affected file: server/routes/brix.js

Description

The /brix/find-by-email/:email endpoint returns decrypted phone numbers and email addresses to any caller with no authentication check:

router.get('/find-by-email/:email', (req, res) => {
  const email = req.params.email.trim().toLowerCase();
  const db = getDb();
  const user = db.prepare(
    'SELECT username, phone, email, nostr_pubkey FROM brix_users WHERE email_hash = ? AND verified = 1'
  ).get(hmacHash(email));

  if (!user) {
    return res.status(404).json({ error: 'Nenhum BRIX encontrado' });
  }

  const domain = process.env.BRIX_DOMAIN || 'brix.app';
  res.json({
    brix_address: `${user.username}@${domain}`,
    username: user.username,
    phone: decrypt(user.phone),
    email: decrypt(user.email),
    has_web_pubkey: user.nostr_pubkey.startsWith('web_'),
  });
});

An attacker can enumerate known or leaked email addresses and receive the decrypted phone number for each matching account.

PoC

GET /brix/find-by-email/brix.monday100@passmail.com HTTP/1.1
Host: brix.brostr.app
(no authentication headers)

HTTP/1.1 200 OK
{
  "brix_address": "brixtest001@brix.brostr.app",
  "username": "brixtest001",
  "phone": null,
  "email": "brix.monday100@passmail.com",
  "has_web_pubkey": true
}

Decrypted email returned to unauthenticated caller. An account with a phone number would also expose the decrypted phone in the same response.

Impact

Full contact data (phone + email) exposed to any unauthenticated caller. Enables targeted PII harvesting at scale.

Remediation

Require NIP-98 HTTP Auth — a Nostr event (kind 27235) signed by the user's private key and sent as an Authorization: Nostr <base64> header. This provides cryptographic proof of key ownership rather than a self-reported header value. Return contact data only when the verified pubkey matches the account owner.

References


VULN-6: Unauthenticated Phone → Nostr Pubkey Correlation via /brix/resolve

Severity: High — CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Affected file: server/routes/brix.js

Description

The /brix/resolve/:query endpoint accepts a phone number or email and returns the associated Nostr public key with no authentication:

router.get('/resolve/:query', (req, res) => {
  // ...
  const rawPhone = query.replace(/\D/g, '');
  if (rawPhone.length >= 8) {
    const candidates = new Set([rawPhone]);
    // ...
    for (const candidate of candidates) {
      user = db.prepare(
        'SELECT username, nostr_pubkey FROM brix_users WHERE phone_hash = ? AND verified = 1'
      ).get(hmacHash(candidate));
      if (user) {
        return res.json({
          found: true,
          brix_address: `${user.username}@${domain}`,
          username: user.username,
          nostr_pubkey: user.nostr_pubkey,
          matched_by: 'phone'
        });
      }
    }
  }
  // ...
});

PoC

GET /brix/resolve/brix.monday100@passmail.com HTTP/1.1
Host: brix.brostr.app
(no authentication headers)

HTTP/1.1 200 OK
{
  "found": true,
  "brix_address": "brixtest001@brix.brostr.app",
  "username": "brixtest001",
  "nostr_pubkey": "web_f51b74dacdf8779efbb002a059479cda",
  "matched_by": "email"
}

The same works for phone number lookups — an attacker enumerating a regional phone range (e.g. all +55 11 9xxxx-xxxx) receives a nostr_pubkey for every registered number.

Impact

Unauthenticated phone/email → Nostr pubkey correlation at scale. Deanonymizes users who intend to keep their phone identity separate from their crypto identity. Also enables the account takeover chain in VULN-7.

Remediation

Require NIP-98 HTTP Auth for phone and email resolution paths. Username-only resolution can remain public since usernames are already public Lightning Addresses. Phone/email lookups must include an Authorization: Nostr <base64> header containing a kind-27235 event signed by the caller's private key — cryptographic proof of key ownership rather than a self-reported header value.

References


VULN-7: Full Account Takeover via Unauthenticated update-contact

Severity: Critical — CVSS v3.1: 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N)

Affected file: server/routes/brix.js

Description

The update-contact endpoint authenticates solely by checking that the x-nostr-pubkey header matches a record in the database:

const user = db.prepare(
  'SELECT * FROM brix_users WHERE nostr_pubkey = ? AND verified = 1'
).get(nostr_pubkey);

No cryptographic proof of key ownership is required. Any caller who knows (or can look up) a victim's nostr_pubkey can initiate a contact change. The OTP is sent to the new contact supplied by the caller — not to the victim's existing contact — so the attacker verifies it themselves.

Steps to Reproduce

  1. Obtain the victim's pubkey: GET /brix/resolve/<victim_phone_or_email> → returns nostr_pubkey (unauthenticated, see VULN-6).
  2. Initiate contact change: POST /brix/update-contact with the victim's nostr_pubkey in the header and the attacker's own email in the body → OTP is sent to the attacker's inbox.
  3. Confirm the change: POST /brix/confirm-update with the attacker's nostr_pubkey header, the OTP, and the attacker's email → server overwrites the victim's email and email_hash in brix_users.
  4. Result: The victim's Lightning Address now resolves to attacker-controlled contact. The attacker can repeat the process to replace the phone number as well.

This chain is made possible by VULN-6 (unauthenticated pubkey resolution) but is independently exploitable if the pubkey is known by any other means (e.g. Nostr relays, prior API responses).

Impact

Full account takeover. The attacker replaces the victim's contact info, hijacks their Lightning Address, and can intercept incoming Bitcoin payments.

Remediation

The x-nostr-pubkey header must be replaced with a real proof of key ownership — for example, a signed challenge via NIP-98 HTTP Auth. Until that is implemented, at minimum send the OTP to the existing contact for confirmation, so the legitimate account holder must approve the change.

References


Disclosure Policy

This report follows a 90-day coordinated disclosure timeline:

  1. Acknowledgement of receipt requested within 7 days.
  2. A fix or mitigation plan within 90 days (by 2026-06-18).
  3. Credit in any public advisory, if the maintainers choose to publish one.

If no response is received within 14 days, the reporter reserves the right to escalate or publish.

Credit

Reporter: jaonoctus

Contact

jaonoctus@protonmail.com

-----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTru4F1HbgJ3lDdWMSsdchrbudDNAUCab1PHwAKCRCsdchrbudD
NEf5AQCiOUCfIc9X/BZssVmAH8r5VgXvvPiCHK7f2TggMA0HqQEAxkZD6yK3FJzT
MTkKl9L2TZEtvONaOBOtmQb/uRF+oAU=
=WTxd
-----END PGP SIGNATURE-----

Comments are disabled for this gist.