From 6e9cc66f47ba7510fbad37e4d459558aec9d81df Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Fri, 9 Oct 2026 10:26:27 +0200 Subject: [PATCH] Add OSS Scanner build environment and threat model --- .oss-scanner/Dockerfile | 56 ++++++++++++++++++ .oss-scanner/Dockerfile.dockerignore | 18 ++++++ .oss-scanner/start-postgres.sh | 7 +++ .oss-scanner/threat_model.md | 85 ++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 .oss-scanner/Dockerfile create mode 100644 .oss-scanner/Dockerfile.dockerignore create mode 100755 .oss-scanner/start-postgres.sh create mode 100644 .oss-scanner/threat_model.md diff --git a/.oss-scanner/Dockerfile b/.oss-scanner/Dockerfile new file mode 100644 index 00000000..e010dff1 --- /dev/null +++ b/.oss-scanner/Dockerfile @@ -0,0 +1,56 @@ +# Build environment for Anthropic's OSS Scanner (https://github.com/anthropics/oss-scanner). +# The scanner builds this image with the repository root as the build context and then audits it without network +# access, so everything needed to run the app and its test suite (PHP + Composer deps, Node deps + built frontend, +# a local PostgreSQL) is installed here. +# +# Inside the finished image: +# .oss-scanner/start-postgres.sh start the local PostgreSQL server (required for tests and the app) +# php artisan test run the PHPUnit suite +# php artisan serve run the app on http://127.0.0.1:8000 (after `php artisan migrate --seed`) + +FROM node:20-bookworm-slim AS node + +FROM php:8.3-cli-bookworm + +ENV DEBIAN_FRONTEND=noninteractive \ + COMPOSER_ALLOW_SUPERUSER=1 \ + COMPOSER_NO_INTERACTION=1 \ + TZ=UTC + +COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/ +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer +COPY --from=node /usr/local/bin/node /usr/local/bin/node +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules +RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ + && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx + +# PostgreSQL 15 (Debian bookworm default) is the database solidtime runs and is tested against. +RUN apt-get update \ + && apt-get install -y --no-install-recommends git unzip curl ca-certificates postgresql postgresql-client \ + && install-php-extensions pdo_pgsql pgsql intl gd zip bcmath exif pcntl sockets soap \ + && rm -rf /var/lib/apt/lists/* \ + && echo "memory_limit=2G" > "$PHP_INI_DIR/conf.d/99-oss-scanner.ini" + +# Database matching .env.ci: user root / password root, database laravel. +RUN pg_ctlcluster 15 main start \ + && runuser -u postgres -- psql -c "CREATE ROLE root WITH LOGIN SUPERUSER PASSWORD 'root';" \ + && runuser -u postgres -- createdb -O root laravel \ + && pg_ctlcluster 15 main stop + +# scanner contract: the checkout lives inside the image, at /src +COPY . /src +WORKDIR /src + +RUN composer install --prefer-dist \ + && npm ci \ + && npm run build + +RUN cp .env.ci .env \ + && php artisan key:generate \ + && php artisan passport:keys --force \ + && chmod +x .oss-scanner/start-postgres.sh + +# Run the test suite so the image is known to work, but do not fail the build on test failures. +RUN .oss-scanner/start-postgres.sh \ + && (php artisan test || echo "WARNING: PHPUnit reported failures") \ + && pg_ctlcluster 15 main stop diff --git a/.oss-scanner/Dockerfile.dockerignore b/.oss-scanner/Dockerfile.dockerignore new file mode 100644 index 00000000..25618c1d --- /dev/null +++ b/.oss-scanner/Dockerfile.dockerignore @@ -0,0 +1,18 @@ +# Used instead of the root .dockerignore when building .oss-scanner/Dockerfile. The root one is tailored to the +# production image and excludes tests, phpunit.xml etc., which the scanner needs. +node_modules +extensions/*/node_modules +vendor +.env +public/build +public/hot +storage/*.key +storage/logs/* +coverage +test-results +playwright-report +.phpunit.cache +.phpunit.result.cache +auth.json +.DS_Store +.idea diff --git a/.oss-scanner/start-postgres.sh b/.oss-scanner/start-postgres.sh new file mode 100755 index 00000000..a7f87a63 --- /dev/null +++ b/.oss-scanner/start-postgres.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Start the local PostgreSQL server used by the test suite and the app (see .oss-scanner/Dockerfile). +set -euo pipefail + +pg_ctlcluster 15 main start 2>/dev/null || true +until pg_isready -h 127.0.0.1 -p 5432 -q; do sleep 0.5; done +echo "PostgreSQL is running on 127.0.0.1:5432 (user root, password root, database laravel)" diff --git a/.oss-scanner/threat_model.md b/.oss-scanner/threat_model.md new file mode 100644 index 00000000..483c21f2 --- /dev/null +++ b/.oss-scanner/threat_model.md @@ -0,0 +1,85 @@ +# Threat model + +## What this project does + +solidtime is an open-source, multi-tenant time tracking web application (Laravel backend, Vue 3 + Inertia frontend, +PostgreSQL). It runs as a hosted SaaS (solidtime.io) and is self-hosted by many organisations. Users belong to one or +more **organizations**; inside an organization each member has a role: `owner`, `admin`, `manager`, `employee` or +`placeholder` (an imported, non-login member). What each role may do is defined in `app/Service/PermissionStore.php`. + +The most important security property is **isolation**: a user must never read or modify data of an organization they +are not a member of, and within an organization a member must not exceed the permissions of their role (e.g. an +employee must not see other members' time entries, billable rates, or manage members, unless the organization settings +explicitly allow it). + +## Where untrusted input enters + +All authenticated users, including employees of any organization and anyone who self-registers (registration is open +by default), are untrusted. + +- **JSON API** `routes/api.php` (`/api/v1/...`), authenticated via Passport (session cookie or personal access token). + Most routes are scoped by `{organization}` and authorised in the controllers / form requests. +- **Public, unauthenticated** endpoints: `GET /api/v1/public/reports` (shared reports, accessed by a secret), login, + registration, password reset, email verification, organization invitation acceptance (`routes/web.php`). +- **Web / Inertia routes** `routes/web.php` and Fortify/Jetstream actions in `app/Actions`. +- **Imports** (`app/Service/Import/Importers`): user-uploaded CSV and ZIP files from Toggl, Clockify, Harvest, + generic CSV and solidtime's own export format. ZIP handling is in `ZipImportHelper.php`. +- **Exports / reports** (`app/Service/Export`, `app/Service/ReportExport`): CSV/XLSX/ODS and PDF. PDFs are rendered by + sending HTML to a Gotenberg (headless Chromium) service, so user-controlled content in that HTML matters. +- **OAuth** (Passport) authorization and token endpoints. +- **Filament admin panel** (`app/Filament`), only for instance super admins (`SUPER_ADMINS` env). Super admins are + trusted. + +## Components that matter most / least + +Most important: organization scoping and role checks in the API controllers, form requests (`app/Http/Requests`), +`PermissionStore`, public report sharing, invitations and member management (role changes, ownership transfer, member +merge), authentication flows (Fortify, 2FA, email change, API tokens), import parsing. + +Less important / out of scope: +- `extensions/` is empty in this repository (proprietary modules are not part of the open-source code). +- `docker/`, `k8s/`, `e2e/`, `playwright/`, `docs/` and developer tooling. +- Third-party dependencies in `vendor/` and `node_modules/`, unless solidtime uses them in an unsafe way. + +## How to exercise it + +- `.oss-scanner/start-postgres.sh` starts the local PostgreSQL server (user `root`, password `root`, db `laravel`). +- `php artisan test` runs the PHPUnit suite. Endpoint tests in `tests/Unit/Endpoint/Api/V1/` show how to create users, + organizations and members with factories and call the API with a given role; they are the quickest way to write a + reproducer. Example: `php artisan test --filter=TimeEntryEndpointTest`. +- To run the app: `php artisan migrate:fresh --seed && php artisan serve` (http://127.0.0.1:8000). Note that the + test suite and the app share the same database. +- There is no network: Gotenberg (PDF generation) and mail delivery are not available. Mail uses the `array` + driver in tests. The 8 PDF export tests in `TimeEntryEndpointTest` fail for this reason; that is expected. + +## How we rate severity + +- **Critical**: unauthenticated access to other users' data or accounts; authentication bypass; remote code execution; + SQL injection reachable by any registered user; reading or writing data of an organization the attacker is not a + member of. +- **High**: privilege escalation within an organization (e.g. employee to admin/owner, or performing admin-only + actions); access to data the role must not see (other members' time entries, billable rates, member emails) when + the organization settings do not allow it; stored XSS that executes in another user's session; SSRF via PDF + rendering or imports; account takeover requiring user interaction. +- **Medium**: information disclosure with limited impact, CSRF on state-changing endpoints, issues requiring an + unusual but realistic configuration, denial of service by a single authenticated request (e.g. pathological + import file). +- **Low**: everything else with real security impact. + +## Anything to leave alone + +Please do not report (see also `SECURITY.md`): +- Theoretical findings without a working reproducer. +- Missing or weak security headers in isolation; TLS / mail DNS configuration. +- Self-XSS; CSRF on non-state-changing endpoints (logout, theme). +- CSV / spreadsheet formula injection in exports. +- Owners or admins acting destructively within their own organization. +- Anything requiring direct DB, shell or filesystem access on a self-hosted instance, or super admin access. +- Missing OAuth scope enforcement (not implemented yet). +- Rate-limit tuning and generic DoS through volume of requests. + +## Reports and patches + +Please include the affected endpoint or code path, the attacker's role and the victim, a PHPUnit test (in the style of +`tests/Unit/Endpoint/Api/V1/`) that reproduces the issue, and a minimal patch that follows the existing patterns +(authorisation in form requests/controllers via `PermissionStore`).