NOORMME (NO-ORM for Normies) takes a security-first approach to database operations. This document outlines the comprehensive security measures implemented and provides best practices for secure usage.
NOORMME is designed with defense in depth and secure by default:
- Multi-layer validation - Security enforced at operation node, parser, and API levels
- Automatic validation of ALL dynamic identifiers at the lowest architectural level
- Safe alternatives to dangerous methods with strong deprecation warnings
- Comprehensive documentation with secure examples
- Built-in protection against common vulnerabilities
- Zero bypass paths - All code paths are secured, not just the documented APIs
NOORMME has undergone a comprehensive security audit that eliminated ALL SQL injection bypass paths:
The Core Security Boundary
All operation nodes (IdentifierNode, TableNode, ColumnNode) now validate inputs at creation time. This is the lowest level of the architecture, ensuring that no code path—documented or undocumented—can bypass security validation.
// BEFORE (VULNERABLE): Direct node creation bypassed validation
ColumnNode.create(userInput) // ❌ No validation!
// AFTER (SECURE): Every node creation is validated
ColumnNode.create(userInput) // ✅ Automatically validated!
// Throws error for: "id; DROP TABLE users--"Files Hardened:
src/operation-node/identifier-node.ts- Base identifier validationsrc/operation-node/table-node.ts- Table name validationsrc/operation-node/column-node.ts- Column name validation
Parser functions like parseTable() and parseStringReference() now rely on validated operation nodes, eliminating the parser bypass vulnerability.
Files Hardened:
src/parser/table-parser.ts- Table parsing with validated nodessrc/parser/reference-parser.ts- Reference parsing with validated nodes
sql.raw() and sql.lit() are now strongly deprecated with critical security warnings:
/**
* @deprecated CATASTROPHICALLY DANGEROUS - This method completely bypasses all security.
* Use safe alternatives from 'noormme/util/safe-sql-helpers' instead.
*/Migration Path: Use safe alternatives:
safeOrderDirection()- For ASC/DESCsafeLimit()/safeOffset()- For paginationsafeKeyword()- For whitelisted keywordssql.ref()/sql.table()/sql.id()- For identifiers (all validated!)
All CLI commands now validate file paths and directory inputs:
// BEFORE (VULNERABLE): Direct path usage
const db = new NOORMME({ database: options.database })
// AFTER (SECURE): Path traversal protection
const dbPath = sanitizeDatabasePath(options.database)
const db = new NOORMME({ database: dbPath })Files Hardened:
src/cli/commands/init.ts- Path validation on initsrc/cli/commands/generate.ts- Output directory validationsrc/cli/commands/inspect.ts- Database path validation
NOORMME implements four layers of protection against SQL injection attacks:
- All user-provided values are automatically parameterized by Kysely
- Values are never interpolated into SQL strings
- Database drivers handle proper escaping and type conversion
The Unbreakable Foundation
Every identifier is validated at the operation node level before any SQL is generated:
// This validation happens AUTOMATICALLY at the lowest level:
IdentifierNode.create(name) // ✅ Validates identifier
TableNode.create(table) // ✅ Validates table name
ColumnNode.create(column) // ✅ Validates column nameWhy This Matters: Even if someone finds an undocumented code path or internal API, they CANNOT bypass validation because it happens at the architectural foundation.
All public APIs enforce validation before creating nodes:
sql.ref(userColumn) // ✅ Validated before node creation
sql.table(userTable) // ✅ Validated before node creation
db.dynamic.ref(userColumn) // ✅ Validated before node creation
db.dynamic.table(userTable) // ✅ Validated before node creationAll dynamic identifiers should be validated against application whitelists:
// ✅ Automatic validation prevents SQL injection
import { sql } from 'kysely'
const userColumn = req.query.sortBy
sql.ref(userColumn) // ✅ Automatically validated!
// Throws error for: "id; DROP TABLE users--"
// ✅ Best practice: Also use whitelisting
const allowedColumns = ['name', 'email', 'created_at']
if (!allowedColumns.includes(userColumn)) {
throw new Error('Invalid column')
}
sql.ref(userColumn) // ✅ Double protectionUse safe alternatives instead of raw SQL:
import { safeOrderDirection, safeLimit } from 'noormme/util/security'
// ❌ Old dangerous way:
sql`SELECT * FROM users ORDER BY name ${sql.raw(req.query.dir)}`
// ✅ New safe way:
sql`SELECT * FROM users ORDER BY name ${safeOrderDirection(req.query.dir)}`The security validator checks for:
- SQL keywords (SELECT, UNION, DROP, etc.)
- SQL comment syntax (-- and /* */)
- Quote characters and escape sequences
- Null bytes and special characters
- Reserved SQLite keywords (PRAGMA, ATTACH, etc.)
- Invalid identifier formats
File operations in CLI commands are protected against path traversal attacks:
- Database file paths
- Output directories for code generation
- Migration file locations
// ❌ Blocked: Path traversal
const dbPath = '../../../etc/passwd' // Throws error
// ❌ Blocked: Absolute paths
const dbPath = '/etc/database.db' // Throws error
// ✅ Allowed: Relative paths in current directory
const dbPath = './data/app.db' // ValidAll user inputs are validated before processing:
- Table names:
validateTableReference() - Column names:
validateColumnReference() - Generic identifiers:
validateIdentifier()
- Database paths:
sanitizeDatabasePath() - Output directories:
validateOutputDirectory() - Migration names:
validateMigrationName()
The RateLimiter class provides protection against brute-force and DoS attacks:
import { RateLimiter } from 'noormme/util/security-validator'
const limiter = new RateLimiter(10, 60000) // 10 attempts per minute
try {
limiter.checkLimit('user_login_' + userId)
// Perform sensitive operation
} catch (error) {
// Rate limit exceeded
}User Input
↓
Application Whitelist Validation (Your Code - Layer 4)
↓
API Methods: sql.ref(), sql.table(), db.dynamic.ref() (Layer 3)
↓
Parser Functions: parseTable(), parseStringReference() (Layer 3)
↓
Operation Nodes: IdentifierNode, TableNode, ColumnNode (Layer 2 - CORE BOUNDARY)
↓ [ALL IDENTIFIERS VALIDATED HERE]
↓
SQL Compilation (Layer 1 - Parameterization)
↓
Database Driver
↓
SQLite Database
No Bypass Paths: All arrows MUST pass through Layer 2 (Operation Nodes), making SQL injection architecturally impossible.
When using db.dynamic.ref() or sql.ref() with user input:
SECURITY UPDATE: While NOORMME now validates ALL identifiers automatically at the operation node level, you should STILL use whitelist validation in your application code for defense in depth.
// ⚠️ PROTECTED BUT NOT RECOMMENDED: Direct user input
// NOORMME will automatically block SQL injection attempts:
async function automaticallyProtected(userColumn: string) {
return await db
.selectFrom('users')
.select(db.dynamic.ref(userColumn)) // ✅ Auto-validated!
.execute()
// Throws error for: "id; DROP TABLE users--"
}
// ✅ BEST PRACTICE: Whitelist validation (Defense in Depth)
async function fullySecure(userColumn: string) {
const allowedColumns = ['id', 'name', 'email', 'created_at']
if (!allowedColumns.includes(userColumn)) {
throw new Error('Invalid column name')
}
return await db
.selectFrom('users')
.select(db.dynamic.ref(userColumn)) // ✅ Double protection!
.execute()
}
// ✅ SAFER: Use TypeScript types for validation
type AllowedColumn = 'id' | 'name' | 'email' | 'created_at'
async function typedQuery(userColumn: AllowedColumn) {
return await db
.selectFrom('users')
.select(db.dynamic.ref(userColumn))
.execute()
}// ❌ UNSAFE: User-controlled identifiers
const orderBy = req.query.sort // Could be "1; DROP TABLE users--"
sql`SELECT * FROM users ORDER BY ${sql.ref(orderBy)}`
// ✅ SAFE: Whitelist approach
const allowedSortColumns = {
'name': 'name',
'email': 'email',
'created': 'created_at'
}
const orderBy = allowedSortColumns[req.query.sort]
if (!orderBy) {
throw new Error('Invalid sort column')
}
sql`SELECT * FROM users ORDER BY ${sql.ref(orderBy)}`// ❌ DANGEROUS: User-controlled paths (NEVER DO THIS)
const dbPath = req.query.database // Could be "../../../etc/passwd"
const db = new NOORMME({
connection: { database: dbPath }
})
// ✅ SECURE: Validate and sanitize paths
import { sanitizeDatabasePath } from 'noormme/util/security-validator'
try {
const dbPath = sanitizeDatabasePath(req.query.database)
const db = new NOORMME({
connection: { database: dbPath }
})
} catch (error) {
// Handle invalid path - blocks path traversal attempts
}Note: The CLI commands (init, generate, inspect) now automatically validate all file paths.
When using CLI commands with user input:
# ❌ UNSAFE: Unvalidated input
npx noormme generate --output "$USER_INPUT"
# ✅ SAFE: Validate before use
npx noormme generate --output "./generated"Sensitive configuration should use environment variables:
// .env file (never commit to git!)
DATABASE_PATH=./data/production.db
DATABASE_ENCRYPTION_KEY=your-secret-key
// .gitignore
.env
*.db
*.sqliteWhen using NOORMME in production:
- All user inputs are validated against whitelists
- Dynamic column/table references use
db.dynamic.ref()with validation - File paths are validated and sanitized
- Environment variables are used for sensitive data
- Database files have appropriate file permissions (600 or 640)
- Error messages don't leak sensitive information
- Rate limiting is implemented for authentication endpoints
- SQL queries are reviewed for injection vulnerabilities
- Dependencies are regularly updated for security patches
- Database backups are encrypted and stored securely
If you discover a security vulnerability in NOORMME:
- DO NOT open a public GitHub issue
- Email security concerns to: https://x.com/bozoeggs
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will respond within 48 hours and work with you to address the issue.
NOORMME follows these security practices:
- Dependency Updates: Regular updates to patch known vulnerabilities
- Security Audits: Periodic code reviews for security issues
- Responsible Disclosure: 90-day disclosure policy for reported vulnerabilities
- Version Support: Security patches for the latest major version
Validates generic identifiers (table names, column names, etc.)
Checks:
- String type and non-empty
- Length ≤ 255 characters
- No SQL keywords or injection patterns
- Format: alphanumeric, underscore, dots only
- No reserved SQLite keywords
Validates table references (table or schema.table)
Additional checks:
- Maximum 2 parts (schema.table)
- Each part validated as identifier
Validates column references (column, table.column, or schema.table.column)
Additional checks:
- Maximum 3 parts (schema.table.column)
- Each part validated as identifier
Validates file paths to prevent path traversal
Checks:
- No parent directory references (..)
- No absolute paths (/ or C:)
- No null bytes or forbidden characters
- Optional extension validation
Validates and sanitizes database file paths
Checks:
- Valid file extension (.db, .sqlite, .sqlite3, .db3)
- Relative path only
- No path traversal
Validates output directories for code generation
Checks:
- Relative path only
- No parent directory references
- Alphanumeric, underscores, hyphens, dots, slashes only
Validates migration names
Checks:
- Length ≤ 100 characters
- Alphanumeric, underscores, hyphens only
NOORMME provides safe alternatives in noormme/util/safe-sql-helpers:
import { safeOrderDirection, safeLimit, safeOffset } from 'noormme/util/safe-sql-helpers'
// ❌ Dangerous:
const direction = req.query.dir // Could be "; DROP TABLE users--"
sql`SELECT * FROM users ORDER BY name ${sql.raw(direction)}`
// ✅ Safe:
const direction = req.query.dir
sql`SELECT * FROM users ORDER BY name ${safeOrderDirection(direction)}` // Validates ASC/DESC onlyimport { safeLimit, safeOffset } from 'noormme/util/safe-sql-helpers'
const page = req.query.page
const limit = req.query.limit
sql`SELECT * FROM users LIMIT ${safeLimit(limit)} OFFSET ${safeOffset((page - 1) * limit)}`import { safeOrderBy } from 'noormme/util/safe-sql-helpers'
const allowedColumns = ['name', 'email', 'created_at']
const orderClauses = [
{ column: req.query.sort, direction: req.query.dir }
]
sql`SELECT * FROM users ORDER BY ${safeOrderBy(orderClauses, allowedColumns)}`✅ SQL Injection via Identifiers - ALL identifiers are validated at operation node level ✅ Parser Bypass Attacks - Parsers use validated operation nodes ✅ Direct Node Creation Exploits - Nodes validate on creation ✅ Path Traversal in CLI - All file paths are validated ✅ Malicious Table/Column Names - Comprehensive pattern matching
- Type Coercion: TypeScript types don't prevent runtime injection - validation happens at runtime
- Legacy Methods:
sql.raw()andsql.lit()are deprecated but not removed - they bypass ALL security - File Permissions: NOORMME doesn't set file permissions - configure your OS appropriately
- Encryption at Rest: SQLite encryption requires extensions (SQLCipher) - not built-in
- Zero Backward Compatibility: Insecure code patterns are intentionally broken - this is a feature, not a bug
- OWASP SQL Injection Prevention Cheat Sheet
- SQLite Security
- Kysely Security
- Node.js Security Best Practices
CRITICAL SECURITY FIXES:
-
Operation Node Validation - Added validation at the lowest architectural level
IdentifierNode.create()now validates all identifiersTableNode.create()now validates all table namesColumnNode.create()now validates all column names- Impact: Eliminates ALL SQL injection bypass paths
-
Parser Security - Hardened parser functions
parseTable()relies on validated TableNode creationparseStringReference()relies on validated ColumnNode creation- Impact: Parser functions can no longer bypass validation
-
Method Deprecation - Strongly deprecated dangerous methods
sql.raw()marked as@deprecatedwith CATASTROPHIC warningsql.lit()marked as@deprecatedwith EXTREME DANGER warning- Impact: Clear migration path to safe alternatives
-
CLI Security - Added path validation to all CLI commands
init,generate,inspectcommands now validate all file paths- Impact: Prevents path traversal attacks via CLI
Architecture Changes:
- Defense in Depth: 4-layer security model (was 2-layer)
- Secure by Default: ALL code paths are now validated (was only documented APIs)
- Zero Legacy Compromise: No backward compatibility for insecure patterns
Migration Required:
- Replace
sql.raw()usage with safe alternatives fromnoormme/util/safe-sql-helpers - Validate file paths when using NOORMME programmatically
- Review dynamic identifier usage (though now auto-protected)
- Basic SQLite ORM functionality
- Parameterized query support
- Limited identifier validation at API level