Skip to content

Latest commit

 

History

History
180 lines (136 loc) · 7.07 KB

File metadata and controls

180 lines (136 loc) · 7.07 KB
layout default
title Security
nav_order 4

Security guide

VCoder's security helpers make the safe pattern the default one. They are a strong first line of defense — not a guarantee. This page documents what each helper does, and, just as importantly, what it does not.

Principle: validation is context-dependent. A value that is safe in one place (an HTML text node) is dangerous in another (a URL, a shell). VCoder gives you context-specific tools; you must pick the right one.

Path traversal (vcoder.path)

from vcoder import vc

vc.contains_traversal("../../etc/passwd")     # True
vc.contains_traversal("%2e%2e/secret")        # True (percent-encoded)
vc.safe_join("/srv/uploads", user_input)      # Path, or UnsafePathError
vc.resolve_within("/srv/uploads", user_input) # resolves + confirms containment
  • Detects .. segments — raw, percent-encoded (%2e/%2f), double-encoded, and Unicode-normalized (NFKC) variants — plus embedded NUL bytes.
  • safe_join / resolve_within fully resolve the path (following symlinks) and confirm it stays within the allowed root.

Limitations. Filesystem access has races (TOCTOU): a path safe at check time can change before use. Symlinks, hardlinks, network and case-insensitive filesystems all complicate matters. Always run with least privilege and, where possible, operate on file descriptors rather than re-resolving paths.

Filenames (vcoder.filesystem)

from vcoder import vc

vc.validate_filename("report.pdf")            # -> "report.pdf"
vc.validate_filename("../evil")               # ValidationError (separator)
vc.sanitize_filename("my:report?.txt")        # -> "my_report_.txt" (lossy)

validate_filename rejects empty/./.., path separators, control and illegal characters, trailing spaces/dots, Windows reserved device names (CON, NUL, COM1...), and over-long names. sanitize_filename is a lossy best-effort that always returns something usable — prefer validating when the exact name matters.

Subprocess (vcoder.subprocess)

from vcoder import vc

vc.run(["git", "status"])
vc.run_checked(["make", "test"])              # raises on non-zero exit
out = vc.run_capture(["echo", untrusted])     # captured stdout, metachars inert
  • Commands are always argument lists. There is no string-command form and shell=True is not exposed, so shell metacharacters in arguments cannot start a subshell.
  • Arguments must be NUL-free strings/path-likes; the executable is resolved and can be constrained with allowed_dirs= to block attacker-planted binaries.

Limitations. Not using a shell stops shell injection, but the program you run may still interpret arguments dangerously (find -exec, ssh ProxyCommand, tar flags). Understand the tool. Never pass untrusted data as options (leading -) unless you have separated them with --.

SQL (vcoder.sql)

from vcoder import vc

query, params = vc.sql.build_select("users", ["id", "email"], {"active": True})
# ("SELECT id, email FROM users WHERE active = ?", [True])
vc.sql.execute(cursor, query, params)

vc.sql.insert(cursor, "users", {"id": 1, "email": "a@b.com"})
  • build_select / build_insert construct statements from validated identifiers and placeholders — values are always bound, never interpolated.
  • execute refuses obviously unsafe queries (leftover {}/%d formatting, a bare string passed as parameters) via guard_query.

Limitations. VCoder does not — and cannot — parse arbitrary SQL. If you build a query string yourself with an f-string, no library can retroactively tell the data from the code. Always pass values as parameters. Identifiers (table/column names) cannot be bound by drivers, so they are checked against a strict allow-list pattern; never feed unvalidated user input as an identifier.

HTML (vcoder.html)

from vcoder import vc

vc.html.escape(user_text)               # safe inside an HTML text node
vc.html.escape_attribute(user_text)     # safe inside a quoted attribute value
vc.html.strip_html(rich_text)           # plain text, tags removed
vc.html.safe_html(untrusted)            # treat input as text, escape everything

Escaping is context-dependent:

  • escape → HTML text content.
  • escape_attribute → inside "..." / '...' attribute values (you supply the quotes).
  • Neither is sufficient inside <script>, <style>, URLs, or unquoted attributes.

Limitations. To render a subset of user HTML (allow some tags), use a dedicated sanitizer such as bleach with an allow-list. safe_html here escapes everything — it never lets any tag through.

Passwords (vcoder.passwords)

from vcoder import vc, PasswordPolicy

policy = PasswordPolicy(min_length=12, require_digit=True, min_bits=50)
vc.validate_password("correcthorsebatterystaple9", policy)   # or PasswordTooWeak

pw = vc.password("Password: ", policy=policy, confirm=True)
  • Configurable length, character-class, entropy, and common-password rules.
  • Optional Have I Been Pwned check (check_pwned=True) using k-anonymity: only the first five characters of the SHA-1 hash leave the process. It is never called unless you opt in.

Limitations. Strength estimation is approximate; the built-in estimate is a simple model. VCoder validates strength — it does not store passwords. Always hash with a slow KDF (argon2/bcrypt/scrypt) before storage, and never log plaintext.

Rate limiting (vcoder.ratelimit)

from vcoder import vc, RateLimitExceeded

# 5 attempts per 5 minutes, tracked per caller.
login_limiter = vc.RateLimiter(max_calls=5, per=300.0)

def login(body, request):
    try:
        login_limiter.check(key=request.client_ip)
    except RateLimitExceeded as exc:
        return 429, {"error": "too many attempts"}, {"Retry-After": int(exc.retry_after)}
    ...  # then validate + verify credentials

# The same limiter can be attached to an interactive prompt:
vc.password("Password: ", rate_limit=login_limiter)

RateLimiter(max_calls, per) allows at most max_calls events within any per-second sliding window. check(key) raises RateLimitExceeded (with retry_after) when the limit is hit; allow(key) is the non-raising form. Passing a key (client IP, user id, ...) gives each caller an independent budget — ideal for slowing down credential-guessing. Attaching a limiter to a prompt via rate_limit= caps how fast that input can be submitted, counting every attempt (including invalid reprompts).

Limitations. The limiter is in-process and in-memory: it does not coordinate across multiple workers or hosts. For distributed rate limiting, back it with a shared store (Redis, etc.) or a dedicated gateway. It throttles frequency; it is not a substitute for authentication or an account-lockout policy.

Logging

Logging is off by default. When enabled (vc.configure(logging=True)), secret prompts route through a redacting path that records only that a secret was received — never the value. Password, and any secret=True, inputs are never echoed and never logged.