Files
ever-gauzy/render.yaml
T
Ruslan KonviserandClaude Fable 5 84032fd11c fix(security): close the residual gaps in the remaining advisories + the LOCAL-provider attachment bug
A re-verification of every draft and published advisory against develop found 9 fully closed and 8
with residuals. This closes the code-fixable ones. Three patterns cover almost all of them:

1. `{ id, ...body }` — the body wins. A path id spread BEFORE the body is overridden by a body `id`,
   and save()/create() with an existing PK is an UPDATE of that row: every authorization check ran
   against the path id while the write hit the body id. On PUT /user/:id that is account takeover
   (a PROFILE_EDIT employee posts {"id":"<SUPER_ADMIN>","hash":"..."}). The id is now pinned LAST in
   all 25 controllers/handlers with that shape, UserService.updateProfile pins entity.id = id, and
   TenantAwareCrudService.create/save/createMany/saveMany refuse an entity whose id already names a
   row of ANOTHER tenant (or a tenant-less row) — the update-through-create endpoints had no other
   ownership check. (GHSA-x4mv-fhwj-g3rp, GHSA-gwpq-mmw7-vx85)

2. A client-supplied value decides an authorization branch. The register handler gated "only a
   SUPER_ADMIN may register a SUPER_ADMIN" on input.user.role.name, and POST /user had no gate at
   all. Both now resolve EVERY role identifier (the flat roleId and the role relation — the relation
   wins on persist) from the database in the caller's tenant and fail closed on an id that does not
   resolve. (GHSA-hjcg-633x-qq74, GHSA-x4mv-fhwj-g3rp)

3. Only the root row is tenant-scoped. SharedEntity turned caller-supplied shareRules.relations
   straight into TypeORM relations on a @Public() token route, so a share of an OWNED Organization
   could pivot featureOrganizations -> feature -> featureOrganizations -> tenant -> organizations ->
   employees -> user into every tenant. Relations are now validated against entity metadata (each hop
   must exist AND target a tenant-scoped entity), depth-bounded, joined rows are scope-filtered,
   tenant-less roots are refused, and create/update bodies are whitelisted. (GHSA-cx2q-xmh2-pc38,
   GHSA-gpg5-qwjc-8hqh)

Also:
- /invite/accept mass-assignment: AuthService.register strips id/hash/emailVerifiedAt/emailToken/
  code/codeExpireAt/refreshToken from input.user, honours createdByUserId only for the authenticated
  caller, pins user.tenantId to the trusted tenant; invite accept pins the invited email.
  (GHSA-929w-5p4w-cxjp)
- TimeOffStatusHandler used raw repositories with no tenant scope (an admin of tenant A could
  approve/deny tenant B's requests); equipment-sharing deleted the request_approval row unscoped, and
  its status change went through an update() that deletes and re-inserts — a { status }-only body
  replaced the record with a stub. (GHSA-gwpq-mmw7-vx85)
- Hubstaff /refresh-token no longer returns the refresh token. (GHSA-3rqg-gpm9-gx84)
- Upload filters on the endpoints that had none: POST /import (archive allowlist), POST
  /ai-chat/attachments and the 5 registry upload routes (script-capable-extension denylist).
  (GHSA-p334-cm7f-php5)
- docker-compose defaults NODE_ENV to production so the insecure-secret guard actually fires (compose
  `environment:` overrode .env.compose and the image ENV); render blueprints generate their secrets
  instead of shipping secretKey/refreshSecretKey/gauzy, and the CORP policy is overridable
  (CORP_POLICY) because API and webapp live on two different *.onrender.com sites.
  (GHSA-chm8-2ggf-pgjq)

And a functional bug found on the way: POST /ai-chat/attachments was broken on the default LOCAL file
provider. It used Nest's @UploadedFile(), which hands over multer's diskStorage object — no `key` (only
core's @UploadedFileStorage() maps it through provider.mapUploadedFile, where LOCAL derives key from
path) — so the service threw 400 AFTER the bytes were written, leaving an orphan. It now uses the core
decorator, never puts a browser-renderable extension on the stored object name, and deletes the stored
object when the upload is rejected (service) or when sniffFile rejects it (docs chat-capture).

PUT /product-types/:id was likewise a silent 400 for every caller (a DTO instance was passed to
EntityManager.save, which resolves metadata from the constructor); it now saves with an explicit
entity target under the verified id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 18:24:20 +02:00

81 lines
2.7 KiB
YAML

services:
- type: web
runtime: image
image:
url: ghcr.io/ever-co/gauzy-api-demo:latest
plan: pro
name: ever-gauzy-api
envVars:
- key: API_HOST
value: 0.0.0.0
- key: API_PORT
value: 3000
- key: DEMO
value: false
- key: NODE_ENV
value: production
# API and webapp live on two different *.onrender.com sites (public suffix), so the
# production default of CORP: same-site would block every API-served image.
- key: CORP_POLICY
value: cross-origin
- key: ADMIN_PASSWORD_RESET
value: true
- key: LOG_LEVEL
value: info
- key: SENTRY_DSN
value: https://7cd381188b6f446ca0e69185227b9031@o51327.ingest.sentry.io/4397292
- key: SENTRY_HTTP_TRACING_ENABLED
value: false
- key: SENTRY_PROFILING_ENABLED
value: false
- key: API_BASE_URL
value: https://ever-gauzy-api.onrender.com
- key: CLIENT_BASE_URL
value: https://ever-gauzy-webapp.onrender.com
- key: EXPRESS_SESSION_SECRET
generateValue: true
- key: JWT_SECRET
generateValue: true
- key: JWT_REFRESH_TOKEN_SECRET
generateValue: true
- key: JWT_VERIFICATION_TOKEN_SECRET
generateValue: true
- key: JWT_REFRESH_TOKEN_EXPIRATION_TIME
value: 86400
- key: ALLOWED_ORIGINS
value: https://ever-gauzy-webapp.onrender.com
healthCheckPath: /api/health
- type: web
name: ever-gauzy-webapp
runtime: image
plan: standard
image:
url: ghcr.io/ever-co/gauzy-webapp-demo:latest
envVars:
- key: DEMO
value: false
- key: CHATWOOT_SDK_TOKEN
value: jFoSXEjGmqhUhqU3zfgkFfMt
- key: CLOUDINARY_API_KEY
value: 256868982483961
- key: SENTRY_DSN
value: https://7cd381188b6f446ca0e69185227b9031@o51327.ingest.sentry.io/4397292
- key: CLOUDINARY_CLOUD_NAME
value: dv6ezkfxg
- key: API_BASE_URL
value: https://ever-gauzy-api.onrender.com
- key: CLIENT_BASE_URL
value: https://ever-gauzy-webapp.onrender.com
- key: GOOGLE_MAPS_API_KEY
value: AIzaSyCJmnKzgTSq5Pc93HXar5bZrdmANTwtSIo
- key: GOOGLE_PLACE_AUTOCOMPLETE
value: true
- key: DEFAULT_LATITUDE
value: 42.6459136
- key: DEFAULT_LONGITUDE
value: 23.3332736
- key: DEFAULT_CURRENCY
value: USD
healthCheckPath: /