← 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.