Web Security / Intermediate: browser and data interpretation defenses / Lesson 8
Injection prevention through data separation
Keep values separate from query, command, and template structure, and review the permissions of the interpreter itself.
On this page
Lesson goals
- Use parameter binding for values and allowlisted structure choices.
- Distinguish safe query construction from authorization and execution evidence.
Name the interpreter and its structure
Injection problems occur when untrusted data is interpreted as part of a language or command structure. SQL, shell commands, template engines, directory queries, and other interpreters have different APIs and rules. Validation alone does not establish safe interpretation. Prefer interfaces that represent values separately from syntax and avoid unnecessary interpreters entirely. A query with safe value binding can still violate authorization if it omits tenant scope. A command with a fixed executable can still be inappropriate if a user controls dangerous options. Review both data separation and the authority granted to the operation.
Bind values rather than formatting SQL text
The local function returns a fixed query and a separate tuple of ordinary values. Its question-mark placeholders illustrate one driver family; actual parameter syntax comes from the selected database interface. The function is not executed against a database, and no engine is started. Its assertion checks that organization and record identifiers remain data rather than being inserted into the SQL structure. A real service must obtain organization from trusted subject context and use the driverโs supported binding API. Do not turn the tuple back into a concatenated string before execution or log private values without a reviewed need.
def note_query(organization, note_id):
sql = "SELECT title FROM note WHERE organization = ? AND id = ?"
return sql, (organization, note_id)
sql, values = note_query("lab-org-a", "lab-note-1")
assert sql == "SELECT title FROM note WHERE organization = ? AND id = ?"
assert values == ("lab-org-a", "lab-note-1")
assert "lab-org-a" not in sql
This program constructs text and a tuple only; it performs no SQL, database connection, or data lookup. It demonstrates value separation and an intended scope predicate, not actual driver behavior or complete access enforcement.
Map allowed choices to fixed syntax
Many interfaces bind data values but not arbitrary table names, column identifiers, operators, or sort directions. If the feature needs a structural choice, map a small approved option to a fixed known fragment instead of accepting raw syntax. The next example chooses between two documented sort contracts and rejects unknown options. It contains no harmful payload. Adding a new option is a reviewed product change with tests for ordering and permissions. Ensure a fallback does not silently widen the query or select a more privileged resource. Keep source values, structural choices, and authorization policy visibly distinct.
def sort_clause(option):
choices = {
"title": "title ASC, id ASC",
"recent": "created_at DESC, id DESC",
}
if option not in choices:
raise ValueError("UNSUPPORTED_SORT")
return choices[option]
assert sort_clause("title") == "title ASC, id ASC"
try:
sort_clause("unlisted-choice")
except ValueError as error:
assert str(error) == "UNSUPPORTED_SORT"
else:
raise AssertionError("UNKNOWN_SORT_ACCEPTED")
The function returns approved constants and executes nothing. Stable id tie-breaking is part of this fictional ordering policy. This is not a parser for arbitrary SQL or a substitute for database permission.
Avoid crossing into shells or executable templates
For external tools, prefer a supported library or a fixed executable with structured arguments and a narrow option policy rather than a shell string. Do not evaluate user expressions or compile user text into application templates merely to offer flexible formatting. Escaping is interpreter-specific and fragile when several parsing layers are composed. Stored procedures or ORMs are not automatically safe if they later construct raw dynamic syntax. Review resource limits, permissions, and output handling as well as the call site. This course explains defensive boundaries and does not launch a command, connect to a database, or demonstrate an operational injection.
Verify separation through the real adapter
A local builder test supports one narrow contract. Authorized integration evidence must verify the actual driver, query shape, bound types, transaction behavior, error handling, and resource scope. Use benign fixtures and legitimate boundary cases instead of targeting a real service. Keep least-privileged data access so one application operation cannot administer unrelated resources. Test unknown structural options and malformed business values separately. Review every raw query or interpreter escape hatch in the codebase. The goal is a predictable data API with explicit authority, not a claim that one prepared statement makes an entire feature immune to every vulnerability.
Common errors and fixes
Parameters are formatted back into SQL text
- Why it happens
- A separate-value API is bypassed before execution.
- How to fix it
- Pass values through the documented driver binding interface.
- Prevent it next time
- Test the adapter boundary and review raw construction.
A sort field is accepted as raw syntax
- Why it happens
- Structural choices are treated like bindable values.
- How to fix it
- Map approved choices to fixed fragments.
- Prevent it next time
- Reject unsupported structural options explicitly.
Safe binding is called authorization
- Why it happens
- Interpretation safety is confused with resource scope.
- How to fix it
- Enforce trusted subject and resource policy independently.
- Prevent it next time
- Include cross-scope denial in verification.
Check your understanding
1. Why cannot every SQL identifier be replaced by an ordinary value parameter?
Show worked solution
Parameters represent values under a driverโs grammar, not arbitrary language structure. Use supported APIs and approved mappings for required structural choices, then verify the actual query and permissions. Do not solve the limitation by concatenating unchecked syntax.
2. A query binds note_id correctly but omits organization. Is it secure?
Show worked solution
Binding addresses interpretation of that value, not tenant authorization. Apply the validated organization scope and operation policy, then test unrelated subjects and unchanged protected state.
Key takeaways
- Separate values from interpreter structure.
- Allowlist necessary structural choices.
- Binding, permission, and runtime evidence are separate.