uttrflow / uttrflow/uttrflow-swift

A .env block with DB_PASSWORD, GITHUB_TOKEN or JWT_SECRET is shown in plain text: the named-secret rule never matches a prefixed or camelCase name

Open
#595 0 comments 0 reactions 0 assignees View on GitHub
area:clipboard bug P0 security
Dominant language
Swift
Stars
4
Forks
17
Avg merge
3h 32m
Merged PRs (30d)
277

Description

## What happens

`Docs/clipboard-secrets.md` says the named-secret rule catches `.env` lines (`API_KEY=…`, `password: …`), and that "a multi-line clip is a document … the shapes above already catch `.env` lines". The rule looks for its keywords (`api_key`, `secret`, `token`, `password`, `passwd`, `pwd`, `private_key`, `access_key`, `auth_token`, `client_secret`, `credential`, `SecretScanners.swift:183-194`) **between word boundaries**. That is the `\b…\b` of the original pattern, kept exactly by `NamedSecretScan` and `WordBreaks`.

A word boundary does not fall between `_` and a letter, or between a lowercase and an uppercase letter. (A hyphen does give a boundary, so `my-password` is found.) So in the two conventions variable names are actually written in, the keyword is found only when it is the **whole** name. `DB_PASSWORD`, `GITHUB_TOKEN`, `JWT_SECRET`, `STRIPE_API_KEY`, `db_password` and `dbPassword` never match. In practice nearly every real variable name has a prefix. (`SMTP_PASS` and other `…_PASS` names are missed for a second reason: `pass` is not one of the keywords.)

A one-line copy with a long value is often masked anyway, but only by accident: the entropy rule sees `NAME=value` as one long base64-alphabet word. A short value, or any multi-line block, has no such fallback.

## Measured (headless, `SecretShapes.matches`; every value assembled at runtime from invented pieces)

| Clip shape | Masked? |
|---|---|
| `PASSWORD=` + 7-character value with digits | yes |
| `API_KEY=` / `SECRET=` / `TOKEN=` + the same | yes |
| `DB_PASSWORD=` + the same | **no** |
| `GITHUB_TOKEN=` / `JWT_SECRET=` / `STRIPE_API_KEY=` + the same | **no** |
| `db_password=` + the same | **no** |
| a 3-line `.env` (`APP_ENV=production`, `DB_PASSWORD=` + 16-character value, `PORT=8080`) | **no** (also for `GITHUB_TOKEN`, `JWT_SECRET`, `STRIPE_API_KEY`, `AWS_SECRET_ACCESS_KEY`, `POSTGRES_PASSWORD`) |
| a 4-line `.env` with `DATABASE_PASSWORD=` + a 28-letter passphrase | **no**, kind `text` |
| `export GITHUB_TOKEN=` + 18-character value, one line | yes (entropy rule) |
| the same followed by a second `export` line | **no** |
| YAML ` db_password: ` + value | **no** |
| `"dbPassword": "` + value + `"` | **no** |

Other misses found in the same pass:

- A connection string with an empty user name, `scheme://:` + password + `@host:port`: the standard password-only Redis form. **Not masked**, because `CredentialledURLScan` requires a non-empty user (`SecretScanners.swift:115-142`).
- An inline credential in a one-line command: `curl -u user:` + password + ` https://…`, `mysql -u root -p` + password, and `PGPASSWORD=` + value + ` psql -h …`. **Not masked**; classified `code`.

## Why it matters

Copying a block out of an `.env` file, a CI settings page or a Compose file is the most common way a developer puts a credential on the clipboard. The doc names this the expensive failure: "a production password legible on a panel opened in meetings and on recorded calls". The existing `export GITHUB_TOKEN=` test in `SecretDetectionTests` passes only because its 18-character value makes the whole line long enough for the entropy rule.

## Acceptance criteria

- A keyword is also found after `_` and at a lowercase-to-uppercase step (`dbPassword`), as it already is after `-`. Whether `pass` joins the keywords (for `…_PASS`) is decided and tested either way. The keyword's own end still needs a boundary, so `passwordless` and `tokenizer` stay out. The reader stays linear, and `SecretShapesOracleTests` (with its oracle updated to the new rule) and `SecretShapesScalingTests` pass.
- Every **no** in the table becomes masked, in one-line and multi-line clips alike.
- `scheme://:password@host` is masked.
- `-u user:password` and an inline `NAME=value command` with a secret-like name are masked, or the decision not to is recorded in `Docs/clipboard-secrets.md` with the reason.
- `SecretDetectionTests` gains these cases with every value built from pieces at runtime, so the history secret scan never sees a whole credential-shaped literal. The `GITHUB_TOKEN` test gains a short-value, multi-line twin.
- The false-positive table still holds: `var password: String`, `passwordless login` and `token_count: 128000` stay unmasked. The PR says what happens to `max_tokens: 4096`, which a keyword-after-underscore rule would newly reach (its value has a digit).

Contributor guide

Open the contributing guide

Research direction

Start with SecretScanners.swift:115-194, especially NamedSecretScan, WordBreaks, and CredentialledURLScan, then read the named cases in SecretDetectionTests. Update the matching behavior and tests for prefixed, camelCase, multiline, URL, and inline credentials while preserving the listed false positives. Run SecretShapesOracleTests, SecretShapesScalingTests, and SecretDetectionTests, and update Docs/clipboard-secrets.md with the resulting decisions.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
documentation, security, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.