Back to skill

Security audit

Webstudio CMS

Security checks across malware telemetry and agentic risk

Overview

This markdown-only skill is not a backdoor, but it documents broad, under-scoped Webstudio admin operations that could expose, overwrite, or publish site data if followed carelessly.

Use this only in an isolated self-hosted Webstudio environment you administer. Before following it on real projects, pin CLI packages and container images, revoke broad anonymous PostgREST access, avoid postgres superuser workflows, require backups and one-row update checks, and do not let an agent read or print AUTH_SECRET unless you explicitly approve that credential use.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
Findings (4)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
api.md:23
Finding
Anonymous PostgREST Role Has Unrestricted Read and Write Access<![CDATA[ ## Vulnerability Details **File Location**: `api.md:23-50, 61-66, 92-98, 122-126`; also documented in `database.md:35-36` **Vulnerability Type**: Broken access control and excessive database privileges **Risk Level**: High ### Vulnerable Code ```markdown ## Verified PostgREST usage The `anon` role has full table access (granted by `db-setup`), so reads AND writes to the tables work without tokens. Tested live from inside the app container: ```bash # read (anon) - verified wget -qO- 'http://postgrest:3000/Build?select=id,projectId&limit=2' wget -qO- 'http://postgrest:3000/Project?select=id,title,domain&limit=3' # write (anon) - verified (returns the inserted row) wget -qO- --post-data='{"name":"x.txt","format":"text/plain","size":10}' \ --header='Content-Type: application/json' --header='Prefer: return=representation' \ 'http://postgrest:3000/File?select=name,format' # RPCs (verified) - see the full RPC list in the OpenAPI spec below wget -qO- --post-data='{"project_id":"<id>","from_build_id":"<id>"}' \ --header='Content-Type: application/json' \ 'http://postgrest:3000/rpc/restore_development_build' # -> "OK" wget -qO- --post-data='{"project_id":"<id>","deployment":"<name>"}' \ --header='Content-Type: application/json' \ 'http://postgrest:3000/rpc/create_production_build' # -> a Build id; sets deployment + PUBLISHED ``` To see the full anon-queryable surface (tables + RPCs), fetch the OpenAPI spec: ```bash wget -qO- http://postgrest:3000/ # swagger 2.0 JSON; paths = tables + /rpc/* ``` ``` The exposed schema includes sensitive and authorization-related tables: ```markdown - `AuthorizationToken` - `{token, projectId, name, relation, canClone, canCopy, canPublish, canUseApi}` - `User`, `Workspace`, `WorkspaceMember` - auth / multi-tenancy ``` ### Technical Analysis PostgREST derives API authorization from PostgreSQL roles, grants, and row-level security policies. Granting the anonymous role unrestricted access to all ...[truncated 2060 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Revoke blanket schema and table privileges from the PostgREST anonymous role. 2. Enable PostgreSQL row-level security on every tenant- or project-scoped table. 3. Prevent anonymous access to `AuthorizationToken`, `User`, `Workspace`, `WorkspaceMember`, domain, and administrative tables. 4. Require validated JWTs for PostgREST access and derive project/workspace scope from authenticated claims. 5. Expose narrowly scoped views or security-reviewed RPCs instead of the entire public schema. 6. Give each service a separate database role with only the operations it requires. 7. Apply explicit authorization checks inside all security-definer RPCs and set a safe `search_path`. 8. Restrict the Compose network so only required services can reach PostgREST. 9. Add automated tests proving unauthenticated requests receive `401` or `403` and cannot enumerate the OpenAPI schema. 10. Rotate authorization tokens if this configuration has ever been reachable by untrusted workloads. ]]>

T08 · Insecure Dependencies

Warning
Location
install.md:38
Finding
Unpinned Packages Are Downloaded and Executed Through npx<![CDATA[ ## Vulnerability Details **File Location**: `install.md:38-51, 80-82` **Vulnerability Type**: Unpinned third-party dependency execution **Risk Level**: Medium ### Vulnerable Code ```markdown ## The CLI Current package is `webstudio`. Run with `npx` (do NOT install globally, do NOT use the old `@webstudio-is/cli` / `wstd`). ```bash npx --yes webstudio@latest --version # verify latest npx webstudio <command> # normal use ``` Needs Node.js 22+. Link a project non-interactively: ```bash npx webstudio link --link "<share-link-with-build-access>" ``` ``` A second unpinned package is also recommended: ```markdown - **Local static preview:** `npx serve .` (required - static files use absolute URLs). ``` ### Technical Analysis `npx` can download and execute packages directly from the configured npm registry. The use of `@latest` explicitly selects mutable package content, while commands without an exact version may resolve according to the current environment or registry state. No lockfile, immutable version, integrity hash, signature verification, or sandbox boundary is specified. Package lifecycle scripts and CLI entry points execute with the invoking user's operating-system privileges. The Webstudio CLI is also supplied with a share link that grants build access. Consequently, a compromised dependency could access both local files and project-authorized credentials or links during execution. ### Attack Path 1. An attacker compromises the package publisher account, npm distribution channel, dependency tree, or a newly released package version. 2. An operator follows the Skill and invokes `npx --yes webstudio@latest`, `npx webstudio`, or `npx serve`. 3. `npx` retrieves the currently resolved package and executes its lifecycle or CLI code. 4. Malicious code reads local project files, environment variables, shell credentials, or command-line arguments. 5. When the link command is used, the malicious package captures the build-acces ...[truncated 705 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin every CLI to an exact reviewed version, such as `webstudio@x.y.z` and `serve@x.y.z`. 2. Maintain a lockfile with npm integrity metadata and review dependency changes before upgrades. 3. Avoid `@latest` in operational instructions and CI pipelines. 4. Prefer a prebuilt, digest-pinned container or a locally installed, reviewed dependency over ad hoc `npx` execution. 5. Disable lifecycle scripts where compatible, for example by using installation controls such as `--ignore-scripts`. 6. Run the CLI in a restricted container with a read-only filesystem, limited network access, and no unrelated host credentials. 7. Use short-lived, project-scoped share links with only the permissions required for the operation. 8. Revoke the share link immediately after use and avoid placing it directly in shell history or process-visible command-line arguments. 9. Verify package provenance and signatures where supported. ]]>

T08 · Insecure Dependencies

Warning
Location
publishing.md:28
Finding
Publisher Container Uses a Mutable latest Image Tag<![CDATA[ ## Vulnerability Details **File Location**: `publishing.md:28-38` **Vulnerability Type**: Mutable and unverified container dependency **Risk Level**: Medium ### Vulnerable Code ```markdown ## Publisher service - Port 4000 (internal): build API - receives publish requests from the builder. - Port 4001 -> host :80: site proxy - serves ALL published sites. - **SSR domains** -> reverse-proxied to their react-router-serve subprocess. - **SSG domains** -> static files served directly from `/var/publish/<domain>/`. - Volumes: `published-sites:/var/publish`, `publisher-work:/var/work`. The publisher is `ghcr.io/webstudio-community/webstudio-publisher:latest`. Health check: `wget -qO- http://127.0.0.1:4000/health`. ``` ### Technical Analysis The `latest` tag is mutable and does not identify a specific reviewed image. A later pull can retrieve different code without any change to the Skill or deployment configuration. The publisher occupies a sensitive position: it receives build requests, accesses project output, writes to persistent publication volumes, and serves all published sites. A compromised registry account, replaced tag, or unsafe upstream release would therefore execute inside the publishing environment and could alter persistent output. This creates a supply-chain trust gap because the effective code can change after the Skill has been audited. ### Attack Path 1. The upstream registry account, build pipeline, or mutable `latest` tag is compromised or replaced. 2. The deployment initially pulls, updates, or recreates the publisher service using `ghcr.io/webstudio-community/webstudio-publisher:latest`. 3. Docker retrieves and runs the changed image. 4. The altered publisher receives build information and gains access to `published-sites` and `publisher-work`. 5. It modifies generated sites, injects client-side payloads, reads reachable service data, or tampers with future publishing operations. ### Impact Assessment A compromised publi ...[truncated 475 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin the publisher image to an immutable OCI digest: ```yaml image: ghcr.io/webstudio-community/webstudio-publisher@sha256:<reviewed-digest> ``` 2. Verify image signatures and provenance before deployment. 3. Scan the exact image digest for vulnerabilities and unexpected binaries. 4. Require explicit review before updating the pinned digest. 5. Run the publisher as a non-root user with a read-only root filesystem. 6. Drop unnecessary Linux capabilities and enable `no-new-privileges`. 7. Mount only the directories required for publishing and separate tenants where practical. 8. Restrict outbound network access and access to unrelated Compose services. 9. Monitor published artifacts for unexpected changes and retain known-good backups. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
database.md:78
Finding
Build Commit Pattern Permits SQL Injection and Cross-Project Draft Overwrites<![CDATA[ ## Vulnerability Details **File Location**: `database.md:78-106` **Vulnerability Type**: SQL injection, unsafe temporary file, and insufficiently scoped database update **Risk Level**: High ### Vulnerable Code ```js const DBC = 'docker compose exec -T db psql -U postgres -d webstudio -t -A -c' const q = (s) => JSON.stringify(s) const load = (col) => execSync(`${DBC} ${q(`select "${col}" from "Build" where deployment is null order by "createdAt" desc limit 1`)}`, { encoding:'utf8' }).trim() let inst = JSON.parse(load('instances')) let styles = JSON.parse(load('styles')) let ss = JSON.parse(load('styleSources')) let ssel = JSON.parse(load('styleSourceSelections')) let props = JSON.parse(load('props')) let pages = JSON.parse(load('pages')) // ...mutate... const rows = { instances: JSON.stringify(inst), styles: JSON.stringify(styles), styleSources: JSON.stringify(ss), styleSourceSelections: JSON.stringify(ssel), props: JSON.stringify(props), pages: JSON.stringify(pages), } const assignments = Object.entries(rows).map(([c,v])=>`"${c}" = $js$${v}$js$`).join(', ') writeFileSync('/tmp/skin.sql', `update "Build" set ${assignments} where deployment is null;\n`) execSync('docker compose exec -T db psql -U postgres -d webstudio < /tmp/skin.sql', {shell:'/bin/zsh'}) ``` The documentation acknowledges the delimiter failure without implementing a safe solution: ```markdown > Gotcha: SQL `$js$...$js$` quoting breaks if a value contains the literal `$js$` > sequence. If you hit that, escape or switch to a different dollar-quote tag. ``` ### Technical Analysis The script serializes build data and interpolates it directly into an SQL statement delimited by `$js$`. PostgreSQL dollar quoting is safe only when the selected delimiter cannot occur in the interpolated value. Build content can contain arbitrary text, HTML, resource data, properties, and user-controlled content. A literal `$js$` therefore terminates the string early. An attacker w ...[truncated 2924 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace generated SQL and shell execution with a PostgreSQL client that supports bound parameters. 2. Pass each JSON document as a query parameter and cast it to the exact target type rather than interpolating it into SQL. 3. Load and retain the exact build ID and project ID, then update only that row: ```sql UPDATE "Build" SET "instances" = $1, "styles" = $2, "styleSources" = $3, "styleSourceSelections" = $4, "props" = $5, "pages" = $6 WHERE "id" = $7 AND "projectId" = $8 AND "deployment" IS NULL; ``` 4. Verify that the affected-row count is exactly one and abort otherwise. 5. Use a transaction and optimistic concurrency control based on `updatedAt` or `version` to prevent lost updates. 6. Do not connect as the `postgres` superuser. Create a role restricted to the required columns and project scope. 7. Avoid temporary SQL files. If one is unavoidable, create it with a secure random name using an API such as `mkdtemp`, set mode `0600`, reject symbolic links, and remove it in a `finally` block. 8. Never attempt to repair this issue solely by choosing a different dollar-quote delimiter; parameterization is the reliable control. 9. Back up the affected build row before mutation and validate project/build identity before committing. 10. Add tests containing `$js$`, quotes, backslashes, HTML, and multiline content to prove that data cannot alter query structure. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (6)

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
This is a real vulnerability because the documentation explicitly describes and validates unauthenticated read/write access to database tables and privileged RPCs through PostgREST using the anon role. In the context of a CMS/admin skill, operationalizing no-token access to project, build, asset, and publish-related functionality materially lowers the barrier to unauthorized modification, data exposure, and site takeover.

Missing User Warnings

High
Confidence
96% confidence
Finding
This is dangerous because it documents unauthenticated write operations and publish-capable RPC calls as working procedures, including examples that can alter data and create production builds. In a skill meant to help administer Webstudio, this goes beyond legitimate use guidance and effectively provides an abuse playbook for unauthorized content changes, destructive actions, and publication of attacker-controlled builds.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
Listing secret-bearing environment variable names is not automatically exploitable, but here the documentation clusters authentication, storage, database, and DNS secrets in a way that meaningfully aids credential hunting and misuse, especially when paired with other insecure operational guidance. The issue is worsened by the lack of strong warnings about handling secrets safely and by the adjacent documentation of broad privileged surfaces those secrets can unlock.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The documented workflow encourages direct SQL updates to the core Build row in production-like data stores and explicitly states that partial writes can corrupt the build, but it does not prominently warn about irreversible damage, backups, transaction safety, or validation. In this skill context, the danger is heightened because the database is the authoritative source for pages, styles, assets, and publishing state, so a mistaken command can break an entire site or publish invalid content immediately.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The document instructs users to read and use `AUTH_SECRET` from `.env` for a browser login flow, but does not explicitly warn that this is a sensitive shared secret or restrict its handling. In an agent skill context, operational instructions are often reused verbatim by automation or less experienced operators, which increases the chance of exposing the secret in logs, screenshots, prompts, or shared transcripts and enables unauthorized access to the builder/editor if the secret leaks.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill explicitly instructs the agent to read AUTH_SECRET from a local .env file and use it to automate login, which normalizes secret access without any user consent, least-privilege guardrails, or warning. In an agentic setting, this can lead to credential harvesting or unauthorized use of authentication material to access protected environments and perform actions as the user.

VirusTotal

VirusTotal findings are pending for this skill version.

View on VirusTotal

Static analysis

No suspicious patterns detected.