Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: 'CodeQL config'

# Scope ANALYSIS to the shipped library source only. This is what actually
# controls what CodeQL extracts and reports on — the workflow's
# `on.*.paths-ignore` only decides whether the workflow runs, not what it
# scans. Allowlisting `packages/*/src` means tests, examples, benchmarks,
# the test-servers, and tooling are never analysed, so findings only ever
# come from code that actually ships.
paths:
- 'packages/*/src'

paths-ignore:
- '**/*.test.js'

queries:
- uses: security-and-quality
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
workflow_dispatch: # manual run: Actions tab → CI → "Run workflow", or `gh workflow run ci.yml`
push:
branches: [master]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: format · test · integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
- run: corepack enable
- run: yarn install --immutable

# format:check (prettier) → unit tests in every workspace → the
# express/fastify 4+5 integration servers (test-servers/run-all-tests.js).
- run: yarn verify
41 changes: 41 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CodeQL

on:
workflow_dispatch: # manual run: Actions tab → CodeQL → "Run workflow", or `gh workflow run codeql.yml`
push:
branches: [master]
# Skip the run entirely for changes that touch no analysed source. What is
# actually SCANNED is scoped in .github/codeql/codeql-config.yml (only
# packages/*/src) — this list just avoids spinning up the job for docs,
# examples, benchmarks, and the test servers.
paths-ignore: &docs-only
- '**/*.md'
- 'examples/**'
- 'benchmarks/**'
- 'test-servers/**'
- 'assets/**'
- 'LICENSE'
pull_request:
branches: [master]
paths-ignore: *docs-only
schedule:
- cron: '0 6 * * 1'

permissions:
contents: read
security-events: write

jobs:
analyze:
name: analyze (javascript-typescript)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript-typescript
# Scope analysis to shipped library source (packages/*/src) so tests,
# examples, benchmarks, the test servers, and tooling are never
# reported on.
config-file: ./.github/codeql/codeql-config.yml
- uses: github/codeql-action/analyze@v3
59 changes: 59 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Release

# Manual publish to npm. Bump versions beforehand through a normal
# "chore: version packages" PR (yarn version:all → commit → PR → merge);
# this workflow only tests and publishes whatever versions currently sit on
# master. Trigger it from Actions → Release → "Run workflow", or
# `gh workflow run release.yml`.

on:
workflow_dispatch:
inputs:
dry_run:
description: 'Test + pack only, do not publish'
type: boolean
default: false

permissions:
contents: read
id-token: write # OIDC — npm Trusted Publishing (no stored token) + provenance

concurrency: release

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- run: corepack enable
- run: yarn install --immutable

# Gate the publish on the full suite — the packages' prepublishOnly hook
# is not run by `yarn npm publish`, so run it explicitly here.
- run: yarn test
- run: yarn test:servers

# OIDC Trusted Publishing — no NPM_TOKEN. Configure a trusted publisher
# for each @exortek/* package on npmjs.com first (repo
# ExorTek/nosql-sanitize, workflow release.yml). Yarn detects the GitHub
# OIDC environment automatically when id-token:write is granted and no
# auth token is set. YARN_NPM_PUBLISH_PROVENANCE attaches build provenance.
# `--topological` publishes core before express/fastify (which depend on
# it via a caret range resolved from npm); `--no-private` skips the root
# and test-servers.
- name: Publish to npm (OIDC Trusted Publishing + provenance)
if: ${{ !inputs.dry_run }}
env:
YARN_NPM_PUBLISH_PROVENANCE: 'true'
run: |
yarn workspaces foreach --all --topological --no-private npm publish --tolerate-republish --access public

- name: Dry-run notice
if: ${{ inputs.dry_run }}
run: echo "dry_run=true — tested only, nothing published."
127 changes: 127 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Contributing to `nosql-sanitize`

Thanks for looking at the code. This document is the shortcut so you don't have to reverse-engineer conventions from the
git log. Keep it open in a tab while you work.

## The packages

A Yarn-workspaces monorepo of three published packages under the `@exortek/*` scope:

| Package | What it is |
|-----------------------------------|------------------------------------------------------------|
| `@exortek/nosql-sanitize-core` | Framework-agnostic sanitization engine. No framework deps. |
| `@exortek/express-mongo-sanitize` | Express 4/5 middleware wrapping the core engine. |
| `@exortek/fastify-mongo-sanitize` | Fastify 4/5 plugin wrapping the core engine. |

`test-servers/` is a private, unpublished workspace: real Express/Fastify apps (v4 + v5) used by the integration suite.

## Prerequisites

| Tool | Version | Why |
|---------|--------------------|-------------------------------------------------------------------------------|
| Node.js | **20.x or newer** | Native test runner (`node --test`). Packages themselves support Node `>=18`. |
| Yarn | **4.x (Corepack)** | The repo pins `packageManager` in `package.json`. Run `corepack enable` once. |
| Git | any recent | Standard. |

## Getting set up

```bash
git clone https://github.com/ExorTek/nosql-sanitize.git
cd nosql-sanitize
corepack enable # once per machine
yarn install
yarn test # unit tests across every workspace
yarn test:servers # Express 4/5 + Fastify 4/5 integration servers
```

If you're only touching one package:

```bash
yarn test:core # or test:express / test:fastify
```

### Framework versions live at the root

The aliased framework packages the tests need — `express` (v5), `express4`, `fastify` (v5), `fastify4` — are declared
**once, in the root `package.json` `devDependencies`**, not inside the publishable packages. The package tests
`require('express')` / `require('express4')` / `require('fastify')` / `require('fastify4')` and resolve them through
workspace hoisting. Do **not** re-add these to `packages/*/package.json`: they are test tooling, and a published package
should not advertise a dev-only framework alias. `test-servers` keeps its own `express4/5` + `fastify4/5` because it is a
private workspace that imports them at runtime.

## Branch naming

One prefix per branch, matching the intent of the change.

| Prefix | Use for | Example |
|-------------|--------------------------------------------|---------------------------------|
| `feat/` | New user-visible functionality. | `feat/allowed-keys-glob` |
| `fix/` | Bug fixes. | `fix/array-index-path` |
| `refactor/` | Internal cleanup, no behaviour change. | `refactor/centralize-guards` |
| `perf/` | Performance work with no behaviour change. | `perf/sanitize-object-loop` |
| `docs/` | README, CONTRIBUTING, JSDoc. | `docs/express-readme` |
| `test/` | Test-only additions or reshuffles. | `test/fastify5-content-type` |
| `chore/` | Repo housekeeping — CI, deps, config. | `chore/hoist-framework-devdeps` |

Keep branch names lower-kebab-case.

## Commit messages

We follow [Conventional Commits](https://www.conventionalcommits.org). One concern per commit. Present tense.

```
<type>(<scope>): <summary>

<body — what and why, wrapped at ~72 cols>
```

- **type** — `feat` · `fix` · `refactor` · `perf` · `docs` · `test` · `chore` · `ci`
- **scope** — the package or subsystem: `core`, `express`, `fastify`, `test-servers`, or omit for repo-wide work.

Avoid `wip`, `fixes stuff`, or squashing several unrelated changes into one commit. Split them.

## The change flow

1. **Branch off `master`.** Use a prefix above.
2. **Write code + tests.** New behaviour is added to `@exortek/nosql-sanitize-core` when it's framework-agnostic; the
Express/Fastify packages should stay thin adapters over the core.
3. **Run the local gate before pushing:**
```bash
yarn verify # format:check + test + test:servers — the CI mirror
```
Use `yarn format` to auto-fix formatting first.
4. **Push, open a PR.** Describe what changed and why. CI (`.github/workflows/ci.yml`) runs `yarn verify` on every push
and PR.

## Testing

Every package uses **Node's native test runner** — no Jest, no Mocha, no Vitest.

- **Unit tests** — `packages/<name>/test/*.test.js`, run by that package's `test` script.
- **Integration** — `test-servers/` boots real Express 4/5 and Fastify 4/5 apps and asserts identical sanitization
behaviour across all four. Run with `yarn test:servers`.

Cover the happy path plus one negative case per branch. A change to sanitization behaviour should touch both a core unit
test and, where the framework boundary matters, the integration servers.

## Style

- **Prettier** is the source of truth (`.prettierrc`): single quotes, `all` trailing commas, 120-col, 2-space indent.
Run `yarn format` (write) or `yarn format:check` (CI gate).
- **Errors** — throw `NoSQLSanitizeError` with a stable `code`. Never string-match error messages downstream.
- **CommonJS.** Packages ship `src/` directly (`"type": "commonjs"`); there is no build step.

## Publishing

Releases are **manual** and run from `master`:

1. Bump versions across the workspaces (`yarn version:all` or edit `package.json` versions deliberately).
2. Publish via the **Release** GitHub Actions workflow (`.github/workflows/release.yml`) — Actions tab → Release →
"Run workflow" — or, as a local fallback, `yarn publish:all`.

Do not publish from a feature branch.

## Reporting security issues

Please **do not** file a public GitHub issue for a security bug. See [`SECURITY.md`](./SECURITY.md) — open a private
GitHub Security Advisory or email [`memet@memet.dev`](mailto:memet@memet.dev).
99 changes: 99 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Security policy

## Supported versions

`@exortek/*` packages follow **semver**. Security fixes land on the current major line of each package; older major
lines are not patched unless the project makes an explicit LTS commitment (none does today).

| Package | Supported |
|-----------------------------------|-----------------|
| `@exortek/nosql-sanitize-core` | `3.x` — current |
| `@exortek/express-mongo-sanitize` | `3.x` — current |
| `@exortek/fastify-mongo-sanitize` | `3.x` — current |

## Reporting a vulnerability

**Do NOT open a public GitHub issue** for anything that could give an attacker a foothold.

### Preferred — GitHub Security Advisories

Use GitHub's private advisory flow:

**<https://github.com/ExorTek/nosql-sanitize/security/advisories/new>**

This routes the report privately to the maintainers, tracks fix progress, and eventually assigns a CVE if warranted.

### Fallback — email

If GitHub advisories don't work for you, send the report to:

**`memet@memet.dev`**

Please **use plain text** — encrypted mail is welcome but not required. Include:

- Affected package(s) + version(s)
- Node.js version, framework (Express / Fastify) + version, and operating system
- A description of the issue and the impact you're worried about
- A proof-of-concept or minimal reproduction, if you have one

If you'd like credit in the eventual advisory, tell us the name / handle to use. If you'd rather stay anonymous, we'll
keep you off the credits.

## What to expect

- **Acknowledgement:** within **72 hours** of receipt (usually much faster).
- **Triage:** we'll confirm whether we can reproduce, agree on severity, and share a rough timeline within **7 days**.
- **Fix window:** we aim to publish a patched release within **30 days** of triage for confirmed high/critical issues,
longer for low-severity ones.
- **Coordinated disclosure:** we publish the advisory once a fixed version is on npm. If you have a public disclosure
date in mind (talk, blog post, etc.) tell us early — we'll do our best to align.

## Scope

**In scope**

- Any published `@exortek/*` package listed under "Supported versions".
- A **sanitizer bypass** — user input that reaches a MongoDB query with operator keys (`$`-prefixed), prototype-pollution
keys (`__proto__` / `constructor` / `prototype`), or dotted keys still intact after the middleware has run.
- The build/publish pipeline (npm tarball contents, `files` integrity).

**Out of scope**

- **Third-party dependencies** — report those to their maintainers. Where a fix would land in one of our packages (e.g.
we're pinning a known-vulnerable version), we do want to hear about it.
- **Misconfiguration on the consumer side** — e.g. mounting the middleware after the route that reads the body, or
passing `allowedKeys` that deliberately re-permit operator keys. The documented behaviour is not a vulnerability.
- **NoSQL injection in code that never runs the sanitizer.** This library sanitizes request data at the framework
boundary; it cannot protect a query built from a source it never saw.
- **Docs typos, missing pages, broken README tables.** Those go through the regular issue tracker.

## Hardening guarantees

Every package in this repository is written to hold these invariants. Deviations are treated as bugs and fall under the
reporting policy above.

- **Operator stripping is unconditional.** `$`-prefixed keys are removed or renamed (per the configured mode) at every
depth of the payload — object bodies, nested objects, and array elements alike.
- **Prototype-pollution defence.** `__proto__` / `constructor` / `prototype` keys (`DANGEROUS_KEYS`) are dropped before
any value is written back, so a crafted body cannot mutate `Object.prototype` through the sanitizer.
- **No `RegExp.lastIndex` leakage.** Shared pattern objects never carry state between calls — a global-flagged regex
cannot make one request's match position bleed into the next (covered by a regression test).
- **Bounded recursion.** `maxDepth` caps traversal so a deeply-nested payload cannot exhaust the stack; the depth guard
is enforced identically for objects and arrays.
- **Every failure carries a machine-readable code.** `NoSQLSanitizeError` exposes a stable `code` so callers branch on
`code`, not on message text that can change across versions.
- **Framework-version parity.** The Express (4 + 5) and Fastify (4 + 5) integration suites (`yarn test:servers`) assert
the same sanitization outcome across every supported framework major, so a bypass cannot hide behind one adapter.

## Safe-harbour

If you're testing in **good faith** against your own installation of our packages, we consider your research authorised:

- Only test against installations you control.
- Do not access or modify data that isn't yours.
- Do not degrade service for other users.
- Do not disclose publicly until we've had a reasonable window (see fix window above) — or 90 days from initial triage,
whichever comes first.

We won't pursue legal action against researchers who play by these rules. We can't extend safe-harbour to third parties
whose systems happen to run our code, so please don't test against them.
Loading
Loading