Security: README regex example allows unintended origin matching
- Dominant language
- JavaScript
- Stars
- 6.2k
- Forks
- 512
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
The README's documented regex example for the `origin` option (`/example\.com$/`) matches unintended origins like `evil-example.com`, creating a CORS bypass when developers follow the documented pattern.
## Reproduction
```javascript
const cors = require('cors');
// Pattern from README line: "For example the pattern /example\.com$/ will reflect
// any request that is coming from an origin ending with 'example.com'."
const app = express();
app.use(cors({ origin: /example\.com$/ }));
```
**Expected:** Only `example.com` and its subdomains are allowed.
**Actual:** Any domain ending in `example.com` is allowed, including attacker-controlled domains.
```
https://example.com → allowed ✓ (intended)
https://sub.example.com → allowed ✓ (intended)
https://evil-example.com → allowed ✓ (UNINTENDED)
https://notexample.com → allowed ✓ (UNINTENDED)
```
The attacker registers `evil-example.com` → gets full CORS access to the API.
## Root Cause
The regex `/example\.com$/` only anchors the end (`$`) but not the beginning. Since browser `Origin` headers include the scheme (`https://evil-example.com`), the substring `example.com` matches anywhere after the scheme.
Interestingly, the test file (`test/test.js:184`) uses the correct pattern:
```javascript
// From test file — properly anchored
{ origin: /:\/\/(.+\.)?example.com$/ }
```
This correctly rejects `evil-example.com` because `://` anchors after the scheme, and `(.+\.)?` requires an optional subdomain preceded by a dot.
## Impact
- **Severity:** Medium — CORS origin bypass via domain registration
- **Scope:** Any application that follows the README's regex example
- **Likelihood:** High — the README example is the first thing developers copy when configuring regex-based origin matching
- **With `credentials: true`:** Attacker's site can make authenticated cross-origin requests and read responses
## Suggested Fix
**Option 1 (minimal):** Update the README example to use the pattern from the test file:
```diff
- For example the pattern `/example\.com$/` will reflect any request
- that is coming from an origin ending with "example.com".
+ For example the pattern `/:\/\/(.+\.)?example\.com$/` will reflect
+ any request from "example.com" or its subdomains.
```
**Option 2 (better):** Add a security note to the `origin` configuration docs:
```markdown
> **⚠️ Security note:** When using RegExp origins, always anchor both the scheme
> and domain boundary to prevent unintended matches. `/example\.com$/` matches
> `evil-example.com` — use `/:\/\/(.+\.)?example\.com$/` instead.
```
**Option 3 (best):** Add a runtime console.warn when a RegExp origin lacks scheme anchoring:
```javascript
if (allowedOrigin instanceof RegExp && !allowedOrigin.source.includes(':\\/\\/')) {
console.warn('cors: RegExp origin %s may match unintended domains. ' +
'Consider anchoring after the scheme (e.g., /:\\/\\/(.+\\.)?example\\.com$/)', allowedOrigin);
}
```
## Verification
```javascript
// Insecure (current README example)
/example\.com$/.test('https://evil-example.com') // true — bypass!
// Secure (current test file pattern)
/:\/\/(.+\.)?example\.com$/.test('https://evil-example.com') // false — blocked
/:\/\/(.+\.)?example\.com$/.test('https://sub.example.com') // true — allowed
/:\/\/(.+\.)?example\.com$/.test('https://example.com') // true — allowed
```
## Environment
- cors version: latest (reviewed from `master` branch)
- Node.js: N/A (documentation issue)
Contributor guide
Assessment
This issue has not been assessed yet.