diff --git a/content/documentation-assets/mermaid-diagram-2026-03-12-163039.png b/content/documentation-assets/mermaid-diagram-2026-03-12-163039.png new file mode 100644 index 00000000..8d6a7dc6 Binary files /dev/null and b/content/documentation-assets/mermaid-diagram-2026-03-12-163039.png differ diff --git a/content/posts/security-at-zoo.md b/content/posts/security-at-zoo.md new file mode 100644 index 00000000..b8e5dc14 --- /dev/null +++ b/content/posts/security-at-zoo.md @@ -0,0 +1,18 @@ +--- +title: 'Security at Zoo' +excerpt: 'Sec' +coverImage: '/kittycad-blog-banner.png' +date: '2026-04-21T13:00:00.000Z' +author: + name: Max Ammann + picture: '/documentation-assets/maxammann.jpeg' +ogImage: + url: '/kittycad.png' +--- + + +## Vulnerability management at Zoo + + + +## Internal fuzzing \ No newline at end of file diff --git a/content/posts/security-dependabot.md b/content/posts/security-dependabot.md new file mode 100644 index 00000000..91222424 --- /dev/null +++ b/content/posts/security-dependabot.md @@ -0,0 +1,153 @@ +--- +title: 'Organization wide dependabot configuration' +excerpt: 'Introducing a tool to configure dependabot across your GitHub organization with better defaults and more customizability.' +coverImage: '/kittycad-blog-banner.png' +date: '2026-05-12T13:00:00.000Z' +author: + name: Max Ammann + picture: '/documentation-assets/maxammann.jpeg' +ogImage: + url: '/kittycad.png' +draft: true +--- + +Dependabot is probably the most widely used tool to automate dependency updates. It is simple and the default on GitHub. + +In this blog post, I want to announce a tool I've been homebrewing for a few months. +It scans your GitHub organization and configures all repositories with better defaults and allows greater customizability. + + +## Simplicity vs customizability + +Dependabot is a fairly simple feature on GitHub. Sometimes it feels a bit neglected by GitHub as it lacks some essential features like org-wide configuration. + +To run Dependabot, you only need to add a `dependabot.yml` file to your repository. However, it needs to be correct. +For instance, if your repository has both JavaScript and Rust code, you need to add two separate entries in the `dependabot.yml` file. +This is hard to maintain. + +The ability to simplify means to eliminate the unnecessary. Maybe GitHub deemed a more automatic ecosystem discovery as unnecessary. More likely, though, +they just haven't gotten around to it yet. In the meantime, we can build a tool that simplifies this process for us and our users. + +Let me introduce [dependabot-org-config](https://github.com/KittyCAD/dependabot-org-config)! + +## The tool + +At its core, it's just a CLI tool that: + +1) Uses GitHub's search API to discover ecosystems (Python, Rust, JavaScript, GitHub Actions, etc.) in your organization. +2) Goes through repositories to create PRs with an updated `dependabot.yml` file that includes all the ecosystems used in the repository. + + +The tool is invoked like this: + +```bash +cargo run -- [--dependabot-overrides ] [--create-pr] [--force-new] [--repo ] [--only-existing] +``` + +You have to specify an organization name and can optionally scope the tool to individual repositories. When testing new configurations, +you may use `--only-existing` to update only existing branches. +Using `--force-new`, repositories that are not yet configured to use Dependabot will be enabled. +Using `--create-pr` will create PRs instead of just pushing branches. + +In our modeling-app repository, you can see an example [dependabot.yml](https://github.com/KittyCAD/modeling-app/blob/main/.github/dependabot.yml). + +For each discovered ecosystem, it will set certain options. The following configuration will: + +1) Perform weekly updates over the weekend to minimize disruption, +2) Group security updates separately from regular version updates (major updates stay independent), +3) Set up a cooldown so we are not immediately affected by recent supply chain attacks, +4) Exclude some of our own packages from updates to avoid breaking changes in our internal dependencies. + +```yaml +# DO NOT EDIT THIS FILE. This dependabot file was generated +# by https://github.com/KittyCAD/ciso Changes to this file should be addressed in +# the ciso repository. + +version: 2 +updates: +- package-ecosystem: cargo + directory: /rust + schedule: + interval: weekly + day: saturday + timezone: America/Los_Angeles + open-pull-requests-limit: 5 + groups: + security: + applies-to: security-updates + exclude-patterns: + - kittycad* + update-types: + - minor + - patch + patch: + applies-to: version-updates + exclude-patterns: + - kittycad* + update-types: + - patch + minor: + applies-to: version-updates + exclude-patterns: + - kittycad* + update-types: + - minor + - patch + cooldown: + default-days: 7 + exclude: + - '*kcl*' + - '*zoo*' + - '*kittycad*' +``` + +One might argue why not just use [renovate](https://github.com/renovatebot/renovate) bot. +As long as you are trying to use its open source version, there is still some configuration overhead if you want org-wide configuration. +Also, I think it's worth noting that Dependabot is just the default tool and therefore a lot of companies will use it. +Its bot reacts nicely to comments, something you only get with [renovate](https://github.com/renovatebot/renovate) when subscribing to their paid plan. + +## Automation + +In one repository, we set up a GitHub action that uses a GitHub app to create PRs. +It can be set up like this: + +```yaml +name: Run Dependabot Org Config +on: + schedule: + - cron: '0 0 1,15 * *' + workflow_dispatch: +jobs: + run-dependabot-org-config: + runs-on: ubuntu-latest + steps: + - name: Checkout this repo + uses: actions/checkout@v6 + + - name: Get GitHub App Token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: id + private-key: key + owner: owner + + - name: Clone dependabot-org-config repo + run: git clone https://github.com/KittyCAD/dependabot-org-config.git ../dependabot-org-config + + - name: Run dependabot-org-config + run: | + export RUST_LOG=info + cd ../dependabot-org-config + cargo run -- KittyCAD --ecosystems-cache .ecosystems-cache.json --dependabot-overrides ../your_repo/dependabot-overrides.toml --create-pr + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} +``` + +## Conclusion + +I think it's a bit sad that Dependabot seems to lack some fundamental features. It would be great if you could +set an organization-wide cooldown for dependency updates. If you are curious why, check out this +blog post about [why we should be all using dependency cooldowns](https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns). + +[dependabot-org-config](https://github.com/KittyCAD/dependabot-org-config) allows security teams to gain some more control about how Dependabot updates dependencies. diff --git a/content/posts/security-orbit.md b/content/posts/security-orbit.md new file mode 100644 index 00000000..7db32a0f --- /dev/null +++ b/content/posts/security-orbit.md @@ -0,0 +1,226 @@ +--- +title: 'Researching foreign cloud-native CAD software' +excerpt: 'A summary of vulnerabilities I found in a cloud-native CAD application and recommendations to fix them.' +coverImage: '/kittycad-blog-banner.png' +date: '2026-05-05T13:00:00.000Z' +author: + name: Max Ammann + picture: '/documentation-assets/maxammann.jpeg' +ogImage: + url: '/kittycad.png' +draft: true +--- + + +## Researching cloud-native CAD vulnerabilities in foreign software + +A while ago I was doing some security researching a cloud-native CAD application. It shares some features with Zoo, like a prompt that allows you to generate CAD designs from text. +They are a small startup so we agreed on using a pseudonym for them and not disclosing their name. Let's name them OrbitSketch. + +We were able to exploit several vulnerabilities ranging from missing their service to actually gaining remote-code-execution on their STEP file exporter. + +This blog post goes through the vulnerabilities and summarizes learnings. Looking at other software, and performing security research on it, +is a great way to find become more creative when looking for security problems in Zoo's systems. + +## ORBIT-1: Minting credits out of thin air + +| Severity | Difficulty | +|----------|------------| +| High | Low | + +Credit deduction is handled on the client side. Attackers can deduct positive or negative credit values, effectively removing or adding credits to their account. + +### Exploit Scenario + +An attacker sends the following request to mint credits. +The "money" parameter is set to -1, effectively adding one credit to the user's account instead of deducting it. + +```bash +curl 'https://someapp.supabase.co/rest/v1/rpc/cash_up' \ + -H "apikey: $API_KEY" \ + -H "authorization: Bearer $BEARER" \ + --data-raw '{"user":"c24acd26-07e4-433c-909c-babb9becbeb8","money":-1,"cad":"851126cb-760d-4e5f-9077-7c11e019edb6"}' +``` + +### Recommendations + +Short term, disallow negative values for the "money" parameter. + +Long term, move logic to deduct credits to the server side entirely, so that the client cannot manipulate it. + +## ORBIT-3: Disclosure of private designs + +| Severity | Difficulty | +|----------|------------| +| High | Low | + +```bash +curl 'https://someapp.supabase.co/rest/v1/cad?select=*&visibility=eq.private' \ + -H "apikey: $API_KEY" \ + -H "authorization: Bearer $BEARER" +``` + +Returns full cad designs not created by the logged in user (the logged in user's id is `c24acd26-07e4-433c-909c-babb9becbeb8`). + +```json +[ + ... + { + "id": "aa03997a-7ef7-4f1d-9df6-334d0ae8f7c1", + "created_at": "", + "updated_at": "", + "user": "", + "title": "", + "description": "", + "visibility": "private", + ... + }, + ... +] +``` + +### Exploit Scenario + +An attacker authenticates as any valid user and queries designs with `visibility=private` using `select=*`. +The response includes private designs belonging to other users, including scad_code and metadata. + +### Recommendations + +Short term, enable Row Level Security (RLS) on the designs table. Configure policies so that private designs are only accessible for owners. Public designs should allow select operations where visibility equals 'public'. +Avoid exposing all table fields; instead create a view (e.g., public_designs) that exposes only non-sensitive fields and excludes scad_code for non-owners. + +Long term, route all design access through a backend service using the Supabase service key. +Enforce ownership checks server-side, add auditing capabilities, and add tests that prevent reading private rows across users. + +## ORBIT-2: Disclosure of user profiles + +| Severity | Difficulty | +|----------|------------| +| High | Low | + +The users table is readable by any authenticated user. +A broad `select=* returns all profile rows and sensitive fields like the full name of the user, enabling user enumeration and privacy violations. + +```bash +curl 'https://someapp.supabase.co/rest/v1/users?select=*' \ + -H "apikey: $API_KEY" \ + -H "authorization: Bearer $BEARER" +``` + +### Exploit Scenario + +An attacker authenticates once and lists all profiles via `select=*. +This data can be scraped to identify users and fetch their public designs. + +### Recommendations + +Short term, disallow fetching other users' profiles by enabling Row Level Security (RLS) on the profiles table. This feature seems not required. + +## ORBIT-4: Raw error stack traces are returned + +| Severity | Difficulty | +|----------|------------| +| Low | Low | + +### Description + +The STEP exporter returns full stack traces and internal error details for malformed inputs. +This might leak implementation details (file paths, package versions, framework internals) and can aid targeted exploitation. + +The following query returns `Translation failed: BRepPrimAPI_MakeRevol failed`. +```bash +curl 'https://step-service-abc.app/export/step' \ + --data-raw '' +``` + +### Exploit Scenario + +An attacker sends malformed or oversized CSG payloads to the exporter. +The service responds with verbose 5xx responses containing stack traces that reveal internal paths and libraries, which can inform targeted probes. + +### Recommendations + +Short term, standardize error handling and return sanitized error messages without stack traces or internal details. + +## ORBIT-5: Frontend source code is not minified + +| Severity | Difficulty | +|----------|------------| +| Low | Low | + +The production frontend serves readable, non-minified JavaScript (and/or exposes source maps). This eases reverse engineering of client logic, endpoints, and endpoints. + +### Exploit Scenario + +An attacker downloads the bundle, reviews human-readable code and comments, extracts internal URLs/keys, and replicates client-side enforcement. + +### Recommendations + +Short term, enable production builds with minification enabled. Disable public source maps in production deployments. + +Long term, treat the client as untrusted by moving all sensitive logic server-side. Avoid embedding secrets in client code and implement server-enforced authorization and rate limits. + +## ORBIT-6: Step generator is unauthenticated + +| Severity | Difficulty | +|----------|------------| +| Medium | Low | + +The Cloud Run STEP export endpoint accepts requests without authentication. Anyone can trigger CPU-heavy conversions, risking abuse, cost amplification, and potential DoS. + +``` +curl 'https://step-service-abc.app/export/step' \ +--data-raw '' +``` + +### Exploit Scenario + +An attacker invokes the endpoint without credentials in a tight loop from multiple clients. This consumes CPU and memory resources, drives up operational costs, and degrades availability for legitimate users. + +### Recommendations + +Short term, require authentication and apply abuse controls. +Long term, place the exporter behind an authenticated backend service that checks user credits and quotas before processing. Process jobs via a queue with per-user limits. Add billing safeguards and monitoring dashboards to track usage patterns. + + + +## ORBIT-7: Arbitrary remote code execution in STEP exporter + +I found a very serious one though. It is possible to execute arbitrary code on https://step-service-abc.app/build123d/execute +As the code is just python you can read from the filesystem, including secrets and environment variables. + +I recommend disabling this endpoint until the execution of it is properly sandboxed. I suspect handling this might be hard to fix immediately. This specific finding is not under the embargo until March, but I'd suggest to start a fresh embargo period so this finding will remain under embargo until Monday, 4 May 2026 + +```bash +curl 'https://step-service-abc.app/build123d/execute' \ + --data-raw $'{"build123d_code":"from build123d import *\\n\\n# Mug dimensions\\nheight = 100.0\\nouter_radius = ... ","output_format":"stl"}' +``` + +### Exploit Scenario + +An attacker invokes the `execute` endpoint with a malicious Python payload that collects sensitive information and sends it out to an attacker-controlled server. +For example, the payload could read environment variables, secrets on the file system, and exfiltrate data via an HTTP request. + +### Recommendations + +Short term, validate the payload to disallow operations that could lead to arbitrary code execution. Block statements like `import os` or `eval()`. +Note that this short-term mitigation likely won't be sufficient and only deters unsophisticated attackers. + +Long-term, implement proper sandboxing for code execution. Use virtual machines and restrict network access and other system calls. + +## Conclusion + +OrbitSketch was very responsive and responsible in handling the vulnerabilities. They fixed all of them within a few weeks and were very transparent about the process, as you can see in the timeline below. +Especially the last vulnerability was quite serious, and it shows how careful you have to be when implementing features at a fast pace. Sandboxing is not optional, and it can be quite difficult to achieve. + +This sparked some ideas on how we can sandbox and separate things even more at Zoo. We are already doing quite extensive sandboxing with our engines, using one K8S node per engine. +However, there is always more sandboxing you can do. Just recently, we implemented sandboxing using Landlock. This allows us to restrict what a process can do during runtime. +Our backend and engine processes drop their privileges after bootstrapping. +After that, they can no longer read from certain file system paths, for example. This is a great mitigation against path traversal vulnerabilities. + +## Disclosure Timeline + +- 18th December 2025: ORBIT-1 through ORBIT-6 were disclosed to OrbitSketch. OrbitSketch acknowledged the vulnerabilities and started working on fixes. +- 5th January 2026: OrbitSketch confirmed that they were fixed. +- 31st January 2026: We validated the fixes for ORBIT-1 through and ORBIT-6. All known issues were fixed except for ORBIT-6. +- 3rd February 2026: ORBIT-6 was fixed. ORBIT-7 was disclosed and mitigated on the same day. diff --git a/content/posts/security-trail-of-bits.md b/content/posts/security-trail-of-bits.md new file mode 100644 index 00000000..7dfdcf3a --- /dev/null +++ b/content/posts/security-trail-of-bits.md @@ -0,0 +1,134 @@ +--- +title: 'Zoo got audited by Trail of Bits (again)' +excerpt: 'What we found in our 2026 and 2024 audits performed by Trail of Bits.' +coverImage: '/kittycad-blog-banner.png' +date: '2026-04-14T13:00:00.000Z' +author: + name: Max Ammann + picture: '/documentation-assets/maxammann.jpeg' +ogImage: + url: '/kittycad.png' +--- + +Secure code audits provide a unique view into the security of a product. +While pentests have become a standard part of the security process for many companies, public secure code audits are still relatively uncommon. +I believe one reason for that is that a lot of security processes evolve around checking the boxes and +"Have you conducted a penetration test in 2025?" is a common question in security questionnaires. + +In this blog post I want to highlight 2 findings from our recent 2026 audit, as well as our 2024 audit, performed by Trail of Bits. +We'll dive deep on one vulnerability and demonstrate how one vulnerability could have been exploited. + +Download our 2024 audit report +[here](https://raw.githubusercontent.com/trailofbits/publications/59ac14c0fd375b6a32ad6a4b02b014a636723e40/reviews/2024-06-zoo-kittycad-securityreview.pdf) +and our 2026 audit report +[here](https://zoo.dev/audit-2025) +. + +## Securing the Rust ecosystem + +One goal of the audit was to evaluate the security of our authentication system which includes SAML and OIDC flows. +We are using the [samael](https://github.com/njaremko/samael) library and sponsor its author for a few years with a monthly donation. +Vasco, one of the Trail of Bits auditors, found several low severity issues in the validation of SAML responses. +None of them were exploitable, but they showed that the current method of validation is not as robust as it could be. + +The library takes the SAML response which is essentially a signed XML document and verifies the signature using xmlsec, +the most widely used library for XML signature verification. So far so good. + +After that however it uses custom logic to find the nodes that have been verified. +This might lead to inconsistencies between what xmlsec verified and what we consider verified. + +Finally, the well-known serde library is used to deserialize the XML document into Rust structs, +which adds another source of parsing differentials. + +In summary, Vasco showed that you can create states where the custom samael logic, xmlsec, +or serde disagree on what has been verified, which could lead to exploitable states. + +The goal of samael is to deserialize only signed data into Rust structs, dropping everything else. So we are reducing the +SAML response which can contain unsigned data into its minimal signed form. +The relevant code implements a function which takes a signed SAML response and returns structured Rust data which you can trust it originates from the SAML IdP. + +Again, nothing was found to be exploitable but we can do better here so we created a Pull Request to fix the issues by +proposing new modes for parsing the signed SAML document after signature verification: + +1. `ValidateAndMark` - Legacy mode which marks the xmlsec returned node, its ancestors and descendants as verified. +2. `ValidateAndMarkNoAncestors` - Similar to the legacy mode but only mark the xmlsec returned node and root ancestor node as verified. +3. `PreDigest` - New strict mode, which gets the XML data just before the digest is calculated. This XML data is guaranteed to contain only data that has been signed. + +Now, which mode works for you likely depends on your IdP. We are hesitant to make the `PreDigest` mode default for all users of the library. +Zoo is switching to the `PreDigest` mode as we know the IdPs which are currently supported and those work with `PreDigest` mode. + +Based on the changes recommended by Trail of Bits, we created a patch that addresses the identified issues. +The version 0.0.20 of `samael` contains the fixes. + + +## Turning path traversal into RCE + +In the 2026 audit, Dominik from Trail of Bits found a path traversal vulnerability in our handling of CAD files. + +Our `/file/conversion` API endpoint allows users to upload 3D files in various formats (STEP, FBX, SLDPRT) for conversion. +The endpoint accepts multipart file uploads where the filename is attached. +We had implemented a `normalize_path` function to sanitize user-provided filenames and prevent path traversal attacks by removing `..` and `.` components. +However, the function had a subtle but serious flaw: it preserved absolute paths. + +When a user uploads a file with a filename like `/etc/cron.d/malicious`, the `normalize_path` function would return it unchanged. +The function correctly handled absolute paths. + +The real problem emerged due to Rust's `PathBuf::join` semantics. When you call `temp_dir.join("/etc/cron.d/malicious")`, +Rust replaces the base path entirely because the argument is absolute. So instead of creating a file at `/tmp/input_abc123/etc/cron.d/malicious`, +the file gets written to `/etc/cron.d/malicious`. + +This vulnerability could have been exploited in at least two ways: + +- **Remote Code Execution**: An attacker could upload an ELF binary or shared library to replace the running process or overwrite libraries loaded by the HOOPS SDK at runtime. +- **DNS Hijacking**: Overwriting `/etc/hosts` or `/etc/resolv.conf` could redirect service-to-service traffic or DNS requests to attacker-controlled servers. + +The exploitation was constrained by two factors. First, Cloudflare WAF blocks some obvious payloads targeting paths like `/etc/cron.d/`. +Second, the attacker needed to find a writable path that would grant them meaningful capabilities. However, the underlying vulnerability remained exploitable +if the WAF was bypassed or if the attacker targeted less obvious paths. + +We resolved this vulnerability by fixing the path normalization logic. Some paths are also directly rejected by our backend now. +Additionally, we are using now kernel-level sandboxing to restrict processes from reading and writing unexpected paths. + + +Internally, we turned this vulnerability into a full-blown exploit that allowed us to pop a shell on our containers. +We used this to validate our fixes, and to demonstrate internally how an attacker could have exploited this vulnerability. +On top of that we are reducing what an attacker could do if they got remote code execution. +We are reducing the availability and lifetime of API credentials throughout the organization. + +## Highlights from the 2024 audit + +In our 2024 review with Trail of Bits, we did not discover a full-blown remote code execution vulnerability. +However, we had several denial-of-service vectors identified, as well as a chain of web related vulnerabilities +that would have allowed an attacker to phish Zoo users. Such a phishing attack would have allowed an attacker +to steal a users session cookie. +Any risks related to these vulnerabilities were mitigated. + +## Beyond audits: Defense in depth + +Source code audits can discover serious vulnerabilities. Doing them once is not enough though, they need to be performed regularly. +Also, they are only one part of a comprehensive security program. + +We have also seen impact in other areas over the past years. +In April 2025, [N008x](https://github.com/N008x) reported through our responsible disclosure program that our Microsoft SSO integration was vulnerable to [nOAuth](https://www.descope.com/noauth). +The bug allows an account takeover. The issue stemmed from trusting the `email` claim +provided by Microsoft instead of using the immutable `sub` (subject) claim as the primary identifier. + +An attacker could create their own Azure AD tenant, add a user with an arbitrary email address (like `victim@example.com`), +and then sign in to Zoo using "Sign in with Microsoft" with those credentials. Because we trusted the email claim without verification, +the attacker would gain access to the victim's account. We fixed this by switching to the `sub` claim as the primary identifier +and implementing proper email verification before account linking. + +We reviewed our logs and validated that the vulnerability was not exploited. We expired all user sessions and unlinked all Microsoft accounts as a precautionary measure. +Now fast-forward to 2026, in the Trail of Bits audit one finding actually referenced this vulnerability and recommended to implement +a mitigating feature that requires email validation for any account linking. +We implemented this feature now. If any of our SSO providers ever has a similar mis-behavior like Microsoft did, we are prepared! + +## Learnings from the audits + +Regular audits are crucial for maintaining security. +I suspect that we will see patterns emerge from the audits over time. +With every new service deployed it probably makes sense to revisit older audits, and check if we +might be vulnerable to similar issues in new parts of our codebase. +One way to reduce the risk is to write static analysis rules and seed LLMs +with previous audits to find attack ideas and validate ideas. + diff --git a/contentlayer.config.js b/contentlayer.config.js index faaac61d..9108395a 100644 --- a/contentlayer.config.js +++ b/contentlayer.config.js @@ -204,6 +204,12 @@ const BlogPost = defineDocumentType(() => ({ description: 'The tags of the blog post, for SEO, categorization, and filtering use.', required: false, }, + draft: { + type: 'boolean', + description: 'Whether the blog post is a draft and should not be published.', + required: false, + default: false, + }, }, computedFields: { slug: {