Add OSS Scanner build environment and threat model

This commit is contained in:
Constantin Graf
2026-10-09 10:26:27 +02:00
parent c994345de9
commit 6e9cc66f47
4 changed files with 166 additions and 0 deletions

56
.oss-scanner/Dockerfile Normal file
View File

@@ -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

View File

@@ -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

7
.oss-scanner/start-postgres.sh Executable file
View File

@@ -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)"

View File

@@ -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`).