← back to Rentv Adintel

docs/SECURITY_ADVERTISER_VIEWER.md

75 lines

# RENTV Advertiser Intelligence — Security Posture

The security controls from the master prompt (§32), and where each is enforced. The theme
is **enforced-in-code, not documented-only**: the SSRF guard, LinkedIn block, and
no-inferred-email rule all have executable tests.

## HTTP hardening

- **Secure headers** on every response (`server.js`): `X-Content-Type-Options: nosniff`,
  `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin`, and
  `x-powered-by` disabled.
- **Content Security Policy** — a conservative CSP served on every page:
  `default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';
  script-src 'self'; base-uri 'self'; form-action 'self'`. Vanilla pages only, no CDNs,
  no third-party script origins.
- **Secure cookies, CSRF protection, server-side authorization, workspace isolation** —
  the auth layer sets secure/SameSite cookies, guards state-changing routes with CSRF
  tokens, and enforces RBAC server-side (never trusting the client). Every workspace's
  data is isolated.

## SSRF guard + LinkedIn block

- Every outbound fetch goes through `lib/compliance/fetch-guard.js`. `assertFetchAllowed`
  rejects:
  - non-http(s) protocols,
  - **LinkedIn hosts** (`linkedin.com`, `www.linkedin.com`, `m.linkedin.com`, `lnkd.in`
    and subdomains) — a hard block (§6.4),
  - any host that resolves (any A/AAAA record) to a **private / loopback / link-local /
    metadata / CGNAT** range — `10/8`, `172.16/12`, `192.168/16`, `127/8`,
    `169.254/16` (incl. the `169.254.169.254` cloud-metadata IP), `0/8`, `100.64/10`,
    and the IPv6 equivalents (`::1`, `fc00::/7`, `fe80::/10`).
- Redirects are followed **manually** and re-checked at every hop, so a redirect to
  LinkedIn or a private IP is blocked too.
- `safeFetch` refuses entirely unless `ALLOW_AUTOMATED_PUBLIC_WEB_RESEARCH === 'true'`,
  sends a descriptive admin-contact User-Agent, and throttles per host.
- Tested in `test/compliance.test.js` (LinkedIn URL, `169.254.169.254`, `127.0.0.1`, and
  `isBlockedIp` range checks).

## No-inferred-email

- `lib/compliance/no-inferred-email.js` — `assertContactEvidence` refuses to store any
  contact lacking a `source_evidence_id` **and** `explicitly_public === true`;
  `looksLikePatternEmail` flags a name-permutation address so importers/UI can reject it.
  Enforced and tested. We never generate an email from a naming pattern.

## Source-policy enforcement

- `lib/compliance/source-policy.js` — `validateSourcePolicy` / `assertSourceEnabledLegal`
  reject any policy that enables automation against a prohibited proprietary DB (CoStar,
  LoopNet, ZoomInfo, Apollo, MLS) or LinkedIn. Automated tests fail when a prohibited
  source is enabled (§6 "create a source-policy validator and automated tests that fail
  when a prohibited source is enabled").

## Uploads & images

- **File size/type limits** and malware-safe upload handling on every upload path.
- **EXIF stripping intent** — uploaded and captured images have EXIF metadata stripped
  (privacy: no embedded GPS/device data); the source URL + timestamp are recorded in
  our metadata, not destructively burned onto the image.
- No unsanitized HTML is rendered; output is encoded.

## Auditing & secrets

- **Audit logs** for imports, exports, evidence changes, source-policy changes, merges,
  and contact changes (`audit_logs`, `export-audit.json` in exports).
- **Secrets are server-only** and redacted from logs — GA4 service-account JSON, Google
  Ads credentials, Gmail OAuth tokens, and search-API keys never appear in client code or
  log output.
- **Secret scanning in CI** guards against committed credentials.

## Click-tracking safety

- Email click-tracking URLs are **stored, not resolved** — the app never auto-visits them
  (that would create false clicks). A human opens them deliberately.