Injection: Queries, Paths, and Logs
Every value that crosses a trust boundary โ into a query, a path, a log line โ needs a barrier. Interpreting user data as structure is the root vulnerability.
The one rule
User input is data, never structure. The whole injection family comes from letting data be parsed as code or structure:
- string-concatenated SQL โ the data becomes SQL,
- unresolved file paths โ the data becomes a location outside the sandbox,
- raw secrets in logs โ the log file becomes the leak.
SQL injection: concatenate = vulnerable
// VULNERABLE: input is pasted into the query text
var sql = "SELECT * FROM Users WHERE Name = '" + username + "'";
// username = "' OR '1'='1" -> returns every row
// username = "'; DROP TABLE Users; --" -> destroys the table
The fix is parameterized queries: send the query text with placeholders and the values separately, so the driver can never re-parse values as SQL:
// SAFE: value travels as data
var sql = "SELECT * FROM Users WHERE Name = @name";
// command.Parameters.AddWithValue("@name", username);
A parameter is honored by every real data API (ADO.NET, EF Core, Dapper). Building a WHERE clause by concatenation is a bug even when "input is validated today" โ validation drifts, parameters don't.
Path traversal: resolve, then confine
Path.Combine(baseDir, userInput) happily produces
/app/data/../../etc/passwd. The defensive shape is resolve first, then
confine: expand the combined path with Path.GetFullPath, then require
the result to still be under the base directory:
var full = Path.GetFullPath(Path.Combine(baseDir, userInput));
bool allowed = full.StartsWith(
Path.GetFullPath(baseDir) + Path.DirectorySeparatorChar);
if (!allowed) throw new UnauthorizedAccessException();
GetFullPath is what collapses .. segments โ checking the unresolved
string catches nothing.
Logs are a public interface
Log lines get aggregated, exported, and read by people and tools with no
business knowing users' secrets. Never log: passwords, tokens, full card
numbers, unmasked personal identifiers. Log what happened, not the
credential that made it happen. If an identifier is needed for correlation,
mask it ("email=***").