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
8 changes: 7 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ Only the latest deployment of samlguy.com is actively maintained.

Please do not open public GitHub issues for security vulnerabilities.

Report via email to: **samlguy@kellenmurphy.com**
**Preferred:** Use GitHub's private vulnerability reporting — [Report a vulnerability](https://github.com/kellenmurphy/samlguy/security/advisories/new). Reports submitted this way are visible only to the maintainer until a Security Advisory is published.

**Alternate:** Email **me@kellenmurphy.com** if you prefer not to use GitHub.

Include:

Expand Down Expand Up @@ -116,6 +118,10 @@ A [Scorecard](https://securityscorecards.dev) workflow runs weekly and on every

The CI workflow enforces test coverage via Vitest and uploads results to Codecov. All library modules (`src/lib/`) maintain 100% statement, branch, function, and line coverage. A drop in coverage fails the build.

### Fuzzing

Property-based fuzz tests (`src/lib/fuzz.test.ts`) run against every parser on every CI build using [fast-check](https://github.com/dubzzz/fast-check). The core invariant tested: for any arbitrary input — random strings, arbitrary byte sequences encoded as base64, or malformed base64 — each parser either returns a valid result or throws an `Error` instance. A non-Error throw (string, plain object, `undefined`) is treated as a test failure. This exercises the SAML decoder, JWT decoder, X.509 DER/ASN.1 parser, and generic fallback decoder against inputs they would never receive in normal use.

---

## License
Expand Down
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@vitest/coverage-v8": "^4.1.5",
"eslint": "^10.3.0",
"eslint-plugin-svelte": "^3.17.1",
"fast-check": "^4.8.0",
"globals": "^17.6.0",
"happy-dom": "^20.9.0",
"jsdom": "^29.1.1",
Expand Down
63 changes: 63 additions & 0 deletions src/lib/fuzz.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it } from 'vitest';
import * as fc from 'fast-check';
import { decodeSaml, decodeAllSaml } from '$lib/saml';
import { decodeJwt } from '$lib/jwt';
import { decodeCert } from '$lib/cert';
import { decodeAllGeneric } from '$lib/generic';

// Invariant: any thrown value must be an Error instance.
// A non-Error throw (string, plain object, undefined, etc.) is a parser bug.
function neverThrowsNonError(fn: () => unknown): boolean {
try {
fn();
return true;
} catch (e) {
return e instanceof Error;
}
}

function bytesToBase64(bytes: Uint8Array): string {
return btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(''));
}

describe('fuzz: decodeSaml', () => {
it('never throws a non-Error for arbitrary string input', () => {
fc.assert(fc.property(fc.string(), (s) => neverThrowsNonError(() => decodeSaml(s))));
});
});

describe('fuzz: decodeAllSaml', () => {
it('never throws a non-Error for arbitrary string input', () => {
fc.assert(fc.property(fc.string(), (s) => neverThrowsNonError(() => decodeAllSaml(s))));
});
});

describe('fuzz: decodeJwt', () => {
it('never throws a non-Error for arbitrary string input', () => {
fc.assert(fc.property(fc.string(), (s) => neverThrowsNonError(() => decodeJwt(s))));
});
});

describe('fuzz: decodeCert — arbitrary base64 strings', () => {
it('never throws a non-Error', () => {
fc.assert(
fc.property(fc.base64String(), (s) => neverThrowsNonError(() => decodeCert(s)))
);
});
});

describe('fuzz: decodeCert — valid base64 of arbitrary byte sequences', () => {
it('never throws a non-Error', () => {
fc.assert(
fc.property(fc.uint8Array({ maxLength: 4096 }), (bytes) =>
neverThrowsNonError(() => decodeCert(bytesToBase64(bytes)))
)
);
});
});

describe('fuzz: decodeAllGeneric', () => {
it('never throws a non-Error for arbitrary string input', () => {
fc.assert(fc.property(fc.string(), (s) => neverThrowsNonError(() => decodeAllGeneric(s))));
});
});
Loading