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
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![lint](https://github.com/hyp3rd/sectools/actions/workflows/lint.yml/badge.svg)](https://github.com/hyp3rd/sectools/actions/workflows/lint.yml) [![test](https://github.com/hyp3rd/sectools/actions/workflows/test.yml/badge.svg)](https://github.com/hyp3rd/sectools/actions/workflows/test.yml) [![security](https://github.com/hyp3rd/sectools/actions/workflows/security.yml/badge.svg)](https://github.com/hyp3rd/sectools/actions/workflows/security.yml)

Security-focused Go helpers for file I/O, in-memory handling of sensitive data, auth tokens, password hashing, and safe numeric conversions.
Security-focused Go helpers for file I/O, in-memory handling of sensitive data, auth tokens, password hashing, input validation/sanitization, and safe numeric conversions.

## Features

Expand All @@ -16,6 +16,8 @@ Security-focused Go helpers for file I/O, in-memory handling of sensitive data,
- Secure in-memory buffers with best-effort zeroization
- JWT/PASETO helpers with strict validation and safe defaults
- Password hashing presets for argon2id/bcrypt with rehash detection
- Email and URL validation with optional DNS/redirect/reputation checks
- HTML/Markdown sanitization, SQL input guards, and filename sanitizers
- Safe integer conversion helpers with overflow/negative guards

## Requirements
Expand Down Expand Up @@ -200,6 +202,69 @@ func main() {
}
```

### Input validation

```go
package main

import (
"context"

"github.com/hyp3rd/sectools/pkg/validate"
)

func main() {
emailValidator, err := validate.NewEmailValidator(
validate.WithEmailVerifyDomain(true),
)
if err != nil {
panic(err)
}

_, _ = emailValidator.Validate(context.Background(), "user@example.com")

urlValidator, err := validate.NewURLValidator(
validate.WithURLCheckRedirects(3),
)
if err != nil {
panic(err)
}

_, _ = urlValidator.Validate(context.Background(), "https://example.com")
}
```

### Sanitization

```go
package main

import (
"github.com/hyp3rd/sectools/pkg/sanitize"
)

func main() {
htmlSanitizer, err := sanitize.NewHTMLSanitizer()
if err != nil {
panic(err)
}

safeHTML, _ := htmlSanitizer.Sanitize("<b>hello</b>")

sqlSanitizer, err := sanitize.NewSQLSanitizer(
sanitize.WithSQLMode(sanitize.SQLModeIdentifier),
sanitize.WithSQLAllowQualifiedIdentifiers(true),
)
if err != nil {
panic(err)
}

safeIdentifier, _ := sqlSanitizer.Sanitize("public.users")

_, _ = safeHTML, safeIdentifier
}
```

## Security and behavior notes

- `ReadFile` only permits relative paths under `os.TempDir()` by default. Use `NewWithOptions` with `WithAllowAbsolute` to allow absolute paths or alternate roots.
Expand Down
17 changes: 17 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
"anchore",
"argon2",
"argon2id",
"Atext",
"Atoi",
"aud",
"autobuild",
"bcrypt",
"behaviour",
"benchmem",
"benchtime",
"bücher",
"bufbuild",
"CODEOWNERS",
"CodeQL",
Expand Down Expand Up @@ -85,9 +87,12 @@
"GOTOOLCHAIN",
"govulncheck",
"honnef",
"hostnames",
"HS256",
"hyperlogger",
"iat",
"IDN",
"idna",
"Infof",
"internalio",
"ints",
Expand All @@ -97,15 +102,19 @@
"jwt",
"Keyfunc",
"kid",
"localhost",
"localmodule",
"markdown",
"memprofile",
"mkdocs",
"mlock",
"mvdan",
"MX",
"myproject",
"mypy",
"myuser",
"nbf",
"nethtml",
"Newf",
"nolint",
"nonamedreturns",
Expand All @@ -126,6 +135,10 @@
"pycache",
"recvcheck",
"Renovate",
"sanitization",
"sanitize",
"sanitizer",
"sanitizers",
"SBOM",
"SBOMs",
"sectauth",
Expand All @@ -137,6 +150,7 @@
"sigstore",
"SLSA",
"softprops",
"sql",
"staticcheck",
"stdlib",
"strconv",
Expand All @@ -146,6 +160,7 @@
"syncdir",
"syncer",
"tagalign",
"tautologies",
"TempDir",
"TempFile",
"TempPrefix",
Expand All @@ -154,6 +169,8 @@
"Tracef",
"uid",
"umask",
"URLHTTP",
"userinfo",
"varnamelen",
"vettool",
"Warnf",
Expand Down
12 changes: 12 additions & 0 deletions docs/security-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ This checklist is a quick reference for teams using sectools in production.
- Rehash stored passwords when `needsRehash` is true.
- Enforce bcrypt's 72-byte limit to avoid silent truncation.

## Input Validation

- Use `pkg/validate` for email/URL parsing instead of ad-hoc regexes.
- Enable DNS verification only when you can tolerate network lookups and timeouts.
- Keep URL schemes restricted and avoid enabling private IPs unless required.

## Sanitization

- Use `pkg/sanitize` for HTML/Markdown sanitization instead of ad-hoc escaping.
- Prefer parameterized SQL queries; use `SQLSanitizer` only for identifiers or literals when needed.
- Use `SQLInjectionDetector` as a heuristic guard for untrusted input before query composition.

## Cleanup

- Use `Remove`/`RemoveAll` to enforce root scoping.
Expand Down
99 changes: 99 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ supporting implementations in `internal/`.
- `pkg/io`: secure file read/write helpers.
- `pkg/auth`: JWT and PASETO helpers with strict validation.
- `pkg/password`: password hashing helpers.
- `pkg/validate`: email and URL validation helpers.
- `pkg/sanitize`: HTML/Markdown sanitizers, SQL input guards, and filename sanitizers.
- `pkg/memory`: secure in-memory buffers.
- `pkg/converters`: safe numeric conversions.
- `internal/io`: implementation details; not part of the public API contract.
Expand Down Expand Up @@ -274,6 +276,103 @@ Behavior:
- `Verify` returns `needsRehash` when parameters or cost drift from the current preset.
- Bcrypt rejects passwords longer than 72 bytes to avoid silent truncation.

## pkg/validate

### Email validation

```go
func NewEmailValidator(opts ...EmailOption) (*EmailValidator, error)
func (v *EmailValidator) Validate(ctx context.Context, input string) (EmailResult, error)
```

Behavior:

- Rejects display names by default; use `WithEmailAllowDisplayName(true)` to permit.
- Validates local part syntax (dot-atom by default); quoted local parts are optional.
- Validates domain labels and length; IDN domains require `WithEmailAllowIDN(true)`.
- Optional DNS verification with `WithEmailVerifyDomain(true)` using MX and optional A/AAAA fallback.

### URL validation

```go
func NewURLValidator(opts ...URLOption) (*URLValidator, error)
func (v *URLValidator) Validate(ctx context.Context, raw string) (URLResult, error)
```

Behavior:

- Enforces `https` only; non-https schemes are rejected (including if configured).
- Rejects userinfo by default; use `WithURLAllowUserInfo(true)` to permit.
- Blocks private/loopback IPs by default; use `WithURLAllowPrivateIP(true)` to permit.
- Optional redirect checks with `WithURLCheckRedirects` and an HTTP client.
- Optional reputation checks with `WithURLReputationChecker`.

## pkg/sanitize

### HTML sanitization

```go
func NewHTMLSanitizer(opts ...HTMLOption) (*HTMLSanitizer, error)
func (s *HTMLSanitizer) Sanitize(input string) (string, error)
```

Behavior:

- Escapes HTML by default (`HTMLSanitizeEscape`).
- Supports stripping tags to plain text with `WithHTMLMode(HTMLSanitizeStrip)`.
- Allows custom policies via `WithHTMLPolicy`.

### Markdown sanitization

```go
func NewMarkdownSanitizer(opts ...MarkdownOption) (*MarkdownSanitizer, error)
func (s *MarkdownSanitizer) Sanitize(input string) (string, error)
```

Behavior:

- Escapes raw HTML by default.
- Allows raw HTML with `WithMarkdownAllowRawHTML(true)`.

### SQL sanitization

```go
func NewSQLSanitizer(opts ...SQLOption) (*SQLSanitizer, error)
func (s *SQLSanitizer) Sanitize(input string) (string, error)
```

Behavior:

- Identifier mode rejects unsafe characters and can allow dotted identifiers.
- Literal mode escapes single quotes using SQL-standard doubling.
- LIKE mode escapes `%`/`_` and the configured escape character.
- Always prefer parameterized queries; sanitization is a safety net.

### SQL injection detection

```go
func NewSQLInjectionDetector(opts ...SQLDetectOption) (*SQLInjectionDetector, error)
func (d *SQLInjectionDetector) Detect(input string) error
```

Behavior:

- Flags common SQL injection patterns (comments, statement separators, tautologies).
- The detector is heuristic; tune patterns if your input includes SQL-like content.

### Filename sanitization

```go
func NewFilenameSanitizer(opts ...FilenameOption) (*FilenameSanitizer, error)
func (s *FilenameSanitizer) Sanitize(input string) (string, error)
```

Behavior:

- Normalizes a single filename or path segment.
- Replaces disallowed characters with a configurable replacement rune.
- Rejects empty results and reserved dot segments.

## pkg/memory

`SecureBuffer` is a public type for holding sensitive data in memory.
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ require (
github.com/hyp3rd/hyperlogger v0.0.8
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
)

require (
aidanwoods.dev/go-result v0.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
2 changes: 2 additions & 0 deletions pkg/sanitize/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package sanitize provides safe-by-default sanitizers for untrusted input.
package sanitize
39 changes: 39 additions & 0 deletions pkg/sanitize/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package sanitize

import "github.com/hyp3rd/ewrap"

var (
// ErrInvalidHTMLConfig indicates an invalid HTML sanitizer configuration.
ErrInvalidHTMLConfig = ewrap.New("invalid html sanitize config")
// ErrInvalidMarkdownConfig indicates an invalid Markdown sanitizer configuration.
ErrInvalidMarkdownConfig = ewrap.New("invalid markdown sanitize config")
// ErrInvalidSQLConfig indicates an invalid SQL sanitizer configuration.
ErrInvalidSQLConfig = ewrap.New("invalid sql sanitize config")
// ErrInvalidFilenameConfig indicates an invalid filename sanitizer configuration.
ErrInvalidFilenameConfig = ewrap.New("invalid filename sanitize config")

// ErrHTMLTooLong indicates the HTML input exceeds the configured limit.
ErrHTMLTooLong = ewrap.New("html input too long")
// ErrHTMLInvalid indicates the HTML input could not be parsed safely.
ErrHTMLInvalid = ewrap.New("html input invalid")
// ErrMarkdownTooLong indicates the Markdown input exceeds the configured limit.
ErrMarkdownTooLong = ewrap.New("markdown input too long")

// ErrSQLInputTooLong indicates the SQL input exceeds the configured limit.
ErrSQLInputTooLong = ewrap.New("sql input too long")
// ErrSQLIdentifierInvalid indicates the SQL identifier is invalid.
ErrSQLIdentifierInvalid = ewrap.New("sql identifier invalid")
// ErrSQLLiteralInvalid indicates the SQL literal is invalid.
ErrSQLLiteralInvalid = ewrap.New("sql literal invalid")
// ErrSQLLikeEscapeInvalid indicates the SQL LIKE escape character is invalid.
ErrSQLLikeEscapeInvalid = ewrap.New("sql like escape invalid")
// ErrSQLInjectionDetected indicates the input matched SQL injection heuristics.
ErrSQLInjectionDetected = ewrap.New("sql injection detected")

// ErrFilenameEmpty indicates the filename is empty after sanitization.
ErrFilenameEmpty = ewrap.New("filename empty")
// ErrFilenameTooLong indicates the filename exceeds the configured limit.
ErrFilenameTooLong = ewrap.New("filename too long")
// ErrFilenameInvalid indicates the filename contains invalid characters.
ErrFilenameInvalid = ewrap.New("filename invalid")
)
Loading
Loading