Disclaimer: This study guide attempts to touch upon the most important topics that may be covered on the exam but does not claim to necessarily cover everything that one needs to know for the exam. Finally, don't take the one hour time window in the title literally.
Last update: Mon Oct 27 11:37:03 2025
Week 5: Hash Pointers, Blockchains, Merkle Trees, and Bitcoin
Cryptographic Foundations
Bitcoin's trust model depends on cryptographic hash functions and authenticated data structures.
A hash function such as SHA-256 converts any input into a fixed-length digest that changes unpredictably when even one bit of input changes.
Hashing enables Bitcoin to verify data integrity, detect tampering, and provide compact digital fingerprints.
A hash pointer is a pointer that also stores a hash of the referenced data.
If the data changes, the hash no longer matches, revealing tampering.
Hash pointers are used in systems such as Git, where each commit points to the hash of its parent commit.
A change in any file causes the commit hash to change, and this change propagates through history.
The blockchain uses the same idea. Each block includes the hash of the previous block's header.
If an attacker modifies one block, every later block becomes invalid because the hashes no longer align.
A Merkle tree organizes data into a binary tree of hashes.
Each internal node stores the hash of its two children, and the root hash commits to all the data below it.
Merkle trees make it possible to verify that a transaction or file is included in a dataset without retrieving everything.
They are used in many systems:
-
Git uses Merkle-like structures to record file contents.
-
BitTorrent uses them to verify downloaded file chunks.
-
Bitcoin uses them to summarize all transactions in a block with a single Merkle root.
The Double-Spending Problem
With physical cash, you cannot spend the same bill twice—once you hand it over, you no longer have it.
Digital files, however, can be copied infinitely.
Without a trusted authority to verify transactions, how do we prevent someone from sending the same bitcoin to two different people?
Traditional systems solve this with a central authority: a bank verifies that you have sufficient funds before approving a transaction.
Bitcoin solves it through its distributed ledger and consensus mechanism: all nodes maintain a complete transaction history and agree on which transactions are valid.
The Distributed Ledger
Bitcoin's ledger is not stored in one place. Instead, it is a distributed ledger: tens of thousands of nodes around the world each maintain a complete, independent copy of the entire transaction history.
This is not a fragmented database where different nodes hold different pieces.
Every participating node stores the full ledger, from the very first transaction in 2009 to the most recent block.
When a new block is added, it propagates across the network, and each node independently verifies and appends it to their local copy.
Nodes can verify they have the correct ledger by comparing block hashes with other nodes.
Because the blockchain is tamper-evident, any discrepancy in block hashes immediately reveals that someone has a corrupted or fraudulent copy.
This redundancy is central to Bitcoin's resilience.
There is no single point of failure, no server to shut down, and no organization that controls the data.
As long as even a handful of nodes remain online, the ledger persists.
The Ledger: Transactions vs. Accounts
Banking systems maintain account balances that change as money moves between accounts.
Bitcoin takes a different approach. It does not track balances but records every transaction ever made.
The current state of the system is the set of unspent transaction outputs (UTXOs).
Each Bitcoin transaction consumes prior outputs as inputs and creates new outputs that represent new ownership records.
Example:
-
Alice sends Bob 0.5 BTC. That transaction gives Bob an output worth 0.5 BTC.
-
Bob later sends Charles 0.2 BTC. His new transaction lists the output from Alice as an input. Because outputs must be spent in full, Bob also creates an output back to himself for 0.3 BTC as change.
This model ensures that:
-
Each output can be spent only once.
-
Every transaction has a clear history.
-
The sum of inputs equals or exceeds the sum of outputs (any remainder is the fee).
Keys and Addresses
Ownership and authorization in Bitcoin rely on public-key cryptography.
Each user generates a key pair:
-
The private key is secret and used to sign transactions.
-
The public key is shared so others can verify those signatures.
There are no usernames or real-world identities on the blockchain.
A user proves ownership simply by producing a valid digital signature with the correct private key.
Bitcoin uses addresses as compact, safer representations of public keys.
Addresses are derived from public keys by hashing them, adding a checksum, and encoding the result in a readable format.
They are used in transaction outputs to specify who can spend a given output.
When a recipient later spends funds, they reveal their public key and signature, allowing others to verify that it matches the address in the earlier transaction.
This system keeps participants pseudonymous while ensuring that only authorized users can spend funds.
Transactions
A Bitcoin transaction contains inputs and outputs. Inputs identify where the bitcoin comes from, and outputs identify to whom it is being transferred.
Each input references an earlier transaction output and provides a digital signature and public key as proof of ownership.
Outputs specify the recipient's address and amount.
Every input must be completely spent, so transactions often include a change output that returns excess funds to the sender.
The small difference between total inputs and outputs becomes the transaction fee, which goes to the miner who includes the transaction in a block.
When a transaction is created, it is broadcast to nearby Bitcoin nodes and propagated across the network within seconds.
Nodes independently verify each transaction by checking signatures, ensuring that referenced outputs exist and have not been spent, and validating the total value.
Once validated, transactions wait in a pool until included in a block.
Blocks and Linking
Transactions are grouped into blocks to simplify verification and synchronization.
A block bundles many transactions and links to the previous block, forming a continuous chain.
Each block has two main parts:
-
Header, which contains metadata such as the previous block hash, Merkle root, timestamp, difficulty target, and nonce.
-
Body, which lists all included transactions.
Changing any transaction alters its hash, which changes the Merkle root, the block hash, and every later block's reference.
Because each block depends on the one before it, the blockchain acts as an append-only, tamper-evident ledger.
What is Mining?
Mining is the process by which new blocks are added to the Bitcoin blockchain.
Miners are specialized nodes that collect valid transactions from the network, bundle them into a candidate block, and compete to publish that block by solving a computational puzzle.
The miner who successfully solves the puzzle first gets to add their block to the chain and receives a reward consisting of:
-
Block reward: newly created bitcoins (currently 3.125 BTC as of 2024)
-
Transaction fees: the sum of all fees from transactions included in the block
Mining serves two critical purposes:
-
It creates new bitcoins in a controlled, predictable way
-
It secures the network by making it computationally expensive to alter transaction history
Proof of Work and the Mining Puzzle
Bitcoin uses Proof of Work to determine which miner can publish the next block.
The mining puzzle requires finding a nonce (a number in the block header) such that the SHA-256 hash of the entire block header is less than a specific threshold called the target hash.
Formally: H(block header) < target hash, where H represents the SHA-256 hash function.
The target hash is a 256-bit number that determines how difficult it is to mine a new block.
The lower the target hash, the harder it is to find a valid solution.
Because hash outputs are unpredictable, miners must try billions or trillions of different nonce values until they find one that produces a hash below the target.
This process is computationally expensive but easy to verify.
Once a miner finds a valid nonce, any node can instantly verify the solution by computing a single hash.
The Difficulty Adjustment Algorithm
To keep the average time between blocks near 10 minutes, Bitcoin automatically adjusts the mining difficulty every 2016 blocks (roughly every two weeks) using the Difficulty Adjustment Algorithm.
If miners collectively produce blocks too quickly, the algorithm decreases the target hash, making the puzzle harder.
If blocks are mined too slowly, it increases the target hash, making it easier.
This self-regulating mechanism ensures that Bitcoin's block production remains stable regardless of how much mining power joins or leaves the network.
Even as miners deploy more powerful hardware, the difficulty adjusts to maintain the 10-minute average.
Mining Hardware Evolution
Bitcoin mining has evolved through several generations of hardware:
-
CPUs were first used in 2009 when anyone could mine on a regular computer.
-
GPUs (graphics cards) quickly replaced them for higher speed.
-
FPGAs (specialized programmable chips) provided greater efficiency.
-
ASICs (Application-Specific Integrated Circuits) dominate modern mining—these are custom chips built solely for SHA-256 hashing and cannot do anything else.
Because finding a valid block hash is probabilistic (like winning a lottery), individual miners often join mining pools to share both the computational work and rewards.
Each miner's chance of success is proportional to their share of the total network computing power.
Consensus and Chain Selection
Nodes always follow the longest valid chain, meaning the chain with the greatest cumulative proof of work (not necessarily the most blocks).
Bitcoin doesn't have a single central authority to decide which chain is correct.
Instead, the network uses consensus mechanisms to ensure all nodes agree on which block represents the head of the chain.
Competing Chains and Forks
When two valid blocks are found nearly simultaneously, the blockchain temporarily splits into competing chains, a situation called a fork.
Because miners are distributed globally and it takes time for blocks to propagate across the network, it's possible for a miner in Asia and a miner in Europe to both find valid blocks at nearly the same moment.
Each broadcasts their block to nearby nodes, and for a short time, different parts of the network may be working on different versions of the chain.
Most miners simply work on whichever valid block they received first.
Over the next few minutes, one branch will likely grow longer as more blocks are added to it.
Once one chain becomes longer, all honest nodes switch to that chain, and the shorter branch is abandoned.
Transactions in the abandoned blocks return to the memory pool and typically get included in future blocks on the winning chain.
This is why Bitcoin transactions are not considered truly final until several blocks have been added after them—a practice called waiting for confirmations.
While accidental forks resolve naturally within minutes, an attacker could attempt to create a competing chain deliberately to reverse a transaction.
However, the computational cost of sustaining a competing chain long enough to overtake the honest chain makes such attacks impractical.
Security and the 51% Attack
For an attacker to modify an earlier transaction, they would need to redo all proof of work from that block onward and surpass the rest of the network.
With thousands of miners contributing massive computational power, catching up is practically impossible.
An attacker who controlled more than half of the total computational power of the network could, in theory, execute a 51% attack—rewriting recent history or excluding specific transactions.
However, the cost of acquiring and operating enough hardware to do this across the global Bitcoin network is so high that such an attack is effectively infeasible in practice.
Even if an attacker succeeded, the attack would likely destroy confidence in Bitcoin, making their stolen coins worthless—a strong economic disincentive.
Mining Rewards and Economics
Each newly mined block includes one special transaction, the coinbase transaction, that creates new bitcoins from nothing.
This is how new coins enter circulation.
The initial reward in 2009 was 50 BTC per block.
Every 210,000 blocks (roughly every four years), it halves:
-
50 bitcoins for the first 4 years
-
25 bitcoins after November 28, 2012
-
12.5 bitcoins after July 9, 2016
-
6.25 bitcoins after May 11, 2020
-
3.125 bitcoins after April 20, 2024
Over time, as the block reward continues to halve, transaction fees are expected to become the main incentive for mining.
After 32 halvings, the reward will reach zero and there will be a maximum of around 21 million bitcoins in circulation.
Miners act honestly because their revenue depends on following the rules.
Any attempt to cheat or fork the chain would destroy their own reward.
This self-interest forms the backbone of Bitcoin's decentralized stability.
System Overview
Bitcoin's architecture combines four reinforcing layers:
| Layer | Purpose |
|---|---|
| Cryptography | Provides data integrity and authorization using hashes and signatures. |
| Data structures | Blockchain and Merkle trees maintain authenticated, tamper-evident storage. |
| Consensus | Proof of Work coordinates the network without central authority. |
| Economics | Block rewards and transaction fees motivate miners to act honestly. |
Key concepts in Bitcoin's design:
-
Hash functions and hash pointers make data tamper-evident
-
Merkle trees efficiently authenticate large sets of data
-
Bitcoin tracks unspent transaction outputs rather than account balances
-
The distributed ledger is maintained by tens of thousands of independent nodes
-
Double-spending is prevented through consensus and transaction verification
-
Proof of Work and the Difficulty Adjustment Algorithm regulate block creation
-
Mining secures the network and distributes new coins
-
Competing chains (forks) resolve naturally through the longest chain rule
-
The target hash determines mining difficulty
-
Economic incentives align miner behavior with network security
Together, these layers allow strangers to agree on a single version of history without a trusted intermediary.
Bitcoin's design shows how cryptography, distributed computing, and incentives can replace institutional trust with mathematical verification.
Week 6: CAPTCHA
CAPTCHA stands for Completely Automated Public Turing test to tell Computers and Humans Apart.
It was designed to identify whether a user is human or an automated program.
It is not a method of authentication but a safeguard to keep automated software from abusing online systems—flooding comment sections, registering fake accounts, spamming, or scraping data at large scale.
The technique is a reverse Turing test—a test designed to verify that the user is human rather than a machine. It takes advantage of human perception: people recognize patterns and context better than early computer vision systems. CAPTCHAs use this difference by presenting visual or auditory tasks that humans can solve easily but machines initially could not.
Early Development
The first CAPTCHA appeared in the late 1990s when AltaVista added a text distortion test to stop automated URL submissions that were skewing search results.
In 2000, researchers formalized the concept with systems that displayed distorted words or generated random text strings with background noise to confuse optical character recognition software.
These early CAPTCHAs worked because of principles from Gestalt psychology, which explains how humans interpret visual information holistically. People naturally fill in missing parts and perceive coherent patterns even in noisy, ambiguous images. Humans could still identify characters despite distortion, clutter, and missing information, while algorithms could not.
Why CAPTCHA Is Still Used
Even with improved security models, websites still need quick ways to detect automation. CAPTCHAs help maintain the integrity and usability of online systems by:
-
Blocking automated abuse: stopping bots that send spam, fill forms, or brute-force passwords.
-
Protecting limited resources: ensuring fair access to trials, API keys, and rate-limited services.
-
Preserving data quality: keeping analytics, polls, and surveys from being polluted by automated input.
-
Maintaining fairness: reducing automated ticket purchases or reservation bots that crowd out real users.
While CAPTCHAs no longer stop every attack, they remain effective at filtering out basic automation.
Problems and Limitations
Over time, the weaknesses of CAPTCHA became apparent:
-
Accessibility: Distorted text is difficult for users with limited vision. Audio CAPTCHAs are often hard to understand.
-
User frustration: Complex puzzles disrupt interaction and are especially inconvenient on phones.
-
Human outsourcing: So-called CAPTCHA farms employ people to solve puzzles on behalf of bots in real time.
-
Improved AI: Machine learning, OCR, and image recognition now solve traditional CAPTCHAs reliably. By the mid-2010s, artificial intelligence could outperform humans at reading distorted text, ending the human advantage.
The result is an arms race: stronger CAPTCHAs frustrate humans more but still fail against advanced bots.
Evolution of CAPTCHA Systems
Text-based CAPTCHAs
Early systems displayed distorted words that humans could read but OCR software could not. Some versions used this human effort productively by having users transcribe text from scanned books that computers couldn't reliably read.
Image-based CAPTCHAs
As text recognition improved, systems shifted to visual recognition tasks:
-
Identifying which images in a grid match a description (e.g., "select all images with bridges")
-
Selecting parts of an image that contain specific objects
-
Dynamic systems that replace images after each click
These image-based puzzles used real-world photos to improve object-labeling accuracy while providing more challenging tests for bots.
Behavioral Analysis (NoCAPTCHA reCAPTCHA)
Modern systems moved away from explicit puzzles to analyzing user behavior.
NoCAPTCHA reCAPTCHA (v2): Introduced a simple checkbox ("I'm not a robot") combined with background analysis of:
-
Browser cookies and session data
-
Mouse movement patterns and timing
-
Click behavior and page interaction
A high confidence score lets users through instantly; low confidence triggers a fallback image puzzle.
Invisible verification: Completely removes user interaction. The system tracks behavior throughout a session and generates a trust score (0 to 1) indicating likelihood the user is human. Websites decide how to respond based on this score.
This approach reduced friction but raised privacy concerns about extensive behavioral tracking.
The AI Threat
By the 2020s, advances in AI nearly eliminated the distinctions between human and automated behavior:
-
In 2024, researchers demonstrated that image-based CAPTCHAs could be defeated with 100% accuracy using object-recognition models and simulated mouse motion.
-
AI agents have passed human verification tests through realistic behavioral mimicry—moving cursors naturally, clicking, and waiting—without solving any puzzles.
Modern AI can convincingly mimic human behavior, erasing the distinction that CAPTCHAs rely on.
New Approaches and Threats
IllusionCAPTCHA
Uses AI-generated optical illusions that people can interpret but current AI systems cannot. Humans passed these tests about 83% of the time; AI models failed completely. This leverages a new asymmetry: humans remain better at interpreting perceptual illusions.
Fake CAPTCHAs as Attacks
Attackers have used imitation CAPTCHA boxes to trick users into running malicious scripts. Fake "I am not a robot" messages have been used to execute hidden commands or install malware, turning a trusted security mechanism into a social engineering vector.
Alternative Verification Methods
Other approaches include:
-
Email or SMS verification: one-time codes sent to known addresses
-
Timing analysis: tracking natural irregularities in human typing and form completion
These methods supplement but don't replace CAPTCHAs and can still be circumvented.
Biometric Verification
As AI becomes indistinguishable from human users, some systems have shifted to physical identity verification using biometric data (such as iris scans) to create cryptographic proof of personhood.
This approach moves from perception-based tests to cryptographic guarantees that a real human is behind an interaction.
However, biometric verification raises significant concerns:
-
Privacy: Biometric data is permanent—unlike passwords, you cannot change it if compromised
-
Surveillance: Centralized databases could enable mass tracking
-
Consent: Biometric verification could become mandatory in some contexts
-
Security: Biometric databases are high-value targets for attackers
The Future of Human Verification
CAPTCHA worked by finding something humans did better than computers. That distinction is disappearing.
Future verification will likely depend on:
-
Behavioral biometrics: tracking natural motion, typing rhythm, and touch interaction patterns
-
Contextual analysis: linking activity to known devices, networks, and usage patterns
-
Cryptographic identity: using verifiable, privacy-preserving tokens that prove an action originates from a legitimate person
The challenge has shifted from proving "I am human" to proving "I am a trustworthy participant."
Week 6: Access Control
Underlying Protection Mechanisms for Access Control
Before studying access control models, it helps to understand the hardware and operating system features that make them possible.
The operating system enforces access control by managing both software and hardware resources. It allocates CPU time through the scheduler, ensuring that no process monopolizes the processor and that all processes get a fair chance to run. It configures the hardware timer, which generates periodic interrupts so the operating system can regain control of the CPU—this enables preemptive multitasking.
It also manages the Memory Management Unit (MMU), setting up each process’s page tables to define what regions of memory it can read, write, or execute. The MMU translates a process’s virtual addresses into physical memory and prevents one process from accessing another’s data.
Processors support at least two privilege levels:
-
Kernel mode (privileged): used by the operating system to execute sensitive instructions, modify memory mappings, and handle interrupts.
-
User mode (unprivileged): used by applications, which must request system services through controlled system calls.
These mechanisms ensure that untrusted code cannot modify the kernel or access protected resources directly. They provide the foundation on which all access control models -- discretionary, mandatory, and role-based -- are built.
Access control
Access control defines what authenticated users and processes are allowed to do. Its purpose is to preserve confidentiality, integrity, and availability by enforcing consistent rules for access to information and system resources.
Different models evolved as computing environments became larger and more complex. Each model solves specific problems but introduces new trade-offs.
The main models are Discretionary Access Control (DAC), Mandatory Access Control (MAC), Role-Based Access Control (RBAC), Attribute-Based Access Control (ABAC), and the Chinese Wall model.
The Access Control Matrix
Access control can be represented as a matrix showing which subjects (users or processes) have which rights over which objects (files, directories, or devices). Although the matrix is a conceptual model, it underlies nearly all practical access control mechanisms.
Two common implementations are:
-
Access Control Lists (ACLs): permissions stored with each object.
-
Capabilities: permissions stored with each subject.
This model inspired later systems such as UNIX file permissions and modern ACL-based operating systems.
Discretionary Access Control (DAC)
Discretionary Access Control gives control to the owner of an object. The owner decides who else can access it and what operations they may perform.
This model encourages sharing but relies heavily on user behavior and trust.
The UNIX / POSIX Model
UNIX and POSIX systems (e.g., Linux, macOS, Android, and the various flavors of BSD) implement DAC through file ownership, groups, and permission bits. Each file has an owner, an owning group, and three sets of permissions for the owner, group, and others.
Permissions are represented by three bits for each category: read (r), write (w), and execute (x).
For example, rwxr-xr-- means the owner can read, write, and execute; the group can read and execute; others can read only.
The chmod command changes permissions, chown changes ownership, and chgrp changes the owning group.
A user’s umask defines which permissions are removed from newly created files, preventing overly permissive defaults.
Each process runs with a real user ID (ruid) and an effective user ID (euid), which determine ownership and access rights.
UNIX uses the setuid and setgid bits to let certain programs temporarily assume the privileges of the file’s owner or group. This is useful for programs that must perform privileged actions, such as changing a password, but it is also risky. Any vulnerability in a setuid program can grant attackers full system privileges.
Modern Linux systems support extended attributes (ACLs), which provide more flexible per-user or per-group permissions than the simple owner-group-other model.
Principle of Least Privilege and Privilege Separation
DAC systems rely on careful assignment of permissions. The principle of least privilege states that each user or process should have only the permissions necessary to complete its task.
Privilege separation divides a program into privileged and unprivileged parts so that most operations run with minimal authority.
For example, a web server may start as root only to bind to a port, then drop privileges for normal operation.
Weaknesses of DAC
DAC is easy to administer and flexible but insecure for high-assurance environments. Users can share data freely, and malicious software can exploit user privileges to leak or destroy data.
DAC cannot enforce consistent organizational or system-wide policies.
Mandatory Access Control (MAC)
Mandatory Access Control enforces a system-wide security policy that users cannot override.
Both subjects and objects have security labels, and access decisions depend on comparing these labels. Users are assigned clearance levels, and resources are assigned classification levels.
This model was originally developed for military and intelligence systems that required strict confidentiality.
Bell–LaPadula Model (Confidentiality)
The Bell–LaPadula (BLP) model is the foundation of what is known as multilevel security (MLS).
In an MLS system, both users (subjects) and data (objects) are labeled with levels such as Unclassified, Confidential, Secret, and Top Secret.
Access decisions depend on these relative levels, enforcing “no read up” and “no write down” to prevent data from flowing from higher to lower classifications.
The Bell–LaPadula model ensures confidentiality through two main rules:
-
Simple Security Property: no read up. A subject cannot read information above its clearance level.
-
Star (★) Property: no write down. A subject cannot write information to a lower classification level.
A third rule, the Discretionary Security Property, allows normal access control lists within the bounds of the mandatory policy.
This ensures information flows only upward in classification, preserving confidentiality.
Weaknesses: The model protects secrecy but not integrity. It also makes collaboration difficult since data cannot be shared downward without special downgrading mechanisms.
Multilateral Security and the Lattice Model
Organizations often require boundaries between peers at the same classification level. Multilateral security extends Bell–LaPadula by dividing each level into compartments, which represent separate projects, missions, or domains of information. A user must have both the appropriate clearance and authorization for the relevant compartment.
For example:
-
A file labeled
{Top Secret, Nuclear}can be accessed only by users cleared for Top Secret and authorized for the Nuclear compartment. -
Another file labeled
{Top Secret, Signals}requires authorization for the Signals compartment. -
A user cleared for
{Top Secret, Nuclear}cannot read{Top Secret, Signals}data even though both are Top Secret.
This approach enforces the need-to-know principle, ensuring that access within the same classification level is limited to those who require it for their duties.
The lattice model formally defines the relationship between classifications and compartments.
Label A dominates label B if its classification level is greater than or equal to B’s and its compartments include those of B. Information can only flow upward in this lattice.
Weaknesses: This approach enforces strong confidentiality but is complex to administer. Managing compartments and clearances becomes difficult at scale, and cross-compartment collaboration is often restricted.
Biba Model (Integrity)
The Biba model complements Bell–LaPadula by protecting data integrity instead of secrecy. It reverses the BLP rules:
-
Simple Integrity Property: no read down. A high-integrity process cannot read low-integrity data.
-
Star (★) Integrity Property: no write up. A low-integrity process cannot modify high-integrity data.
This ensures that untrusted input cannot corrupt trusted data or processes. Biba models appear in applications where data accuracy matters more than confidentiality, such as medical or financial systems.
Microsoft Mandatory Integrity Control implements a simplified version of Biba in Windows.
It labels processes and objects as Low, Medium, High, or System integrity and enforces a no write up rule. Microsoft Mandatory Integrity Control prevents low-integrity programs like browsers from altering system files. Later versions of Windows integrated it into the AppContainer framework, combining it with capability-based restrictions.
Weaknesses: Biba models may block legitimate data flow and are rarely practical for general-purpose systems. Like Bell–LaPadula, they are most effective in specialized environments.
Type Enforcement (SELinux)
Type Enforcement provides a practical way to apply MAC in general-purpose systems.
Processes run in domains, and files and resources are labeled with types.
A policy defines which domains may access which types and how.
For example, a web server in domain httpd_t may read only files labeled httpd_sys_content_t.
This prevents compromised software from affecting other parts of the system.
Type Enforcement forms the basis of SELinux, SEAndroid, and similar systems. It allows fine-grained control and isolation but can be difficult to configure and audit.
Type Enforcement provides the strongest form of containment in modern UNIX systems and remains the most practical way to apply MAC in general-purpose environments.
Role-Based Access Control (RBAC)
RBAC shifts control from individual users to organizational roles. Permissions are assigned to roles, and users acquire permissions by being assigned to roles.
A user may activate one or more roles during a session, depending on the task. A user’s effective permissions are determined by the roles that are active in that session; inactive roles contribute no privileges.
Roles correspond to job functions such as Doctor, Nurse, Manager, or Auditor.
This model scales well in large organizations where many users perform similar tasks.
Role hierarchies allow senior roles to inherit permissions from junior ones.
Constraints enforce separation of duties, ensuring that no single person can perform conflicting actions such as initiating and approving a transaction.
RBAC fits naturally with business structures and enables consistent policy enforcement across systems. It is used in databases, enterprise directories, and cloud identity management.
Weaknesses: RBAC works well for static organizations but cannot express context such as time, location, or risk level. Large deployments may suffer from role explosion when too many narrowly defined roles are created.
Attribute-Based Access Control (ABAC)
ABAC extends RBAC by using attributes instead of fixed roles to make access decisions. Attributes describe properties of users, objects, and the environment, allowing flexible and context-sensitive control.
An example policy might be:
Allow access if the user's department equals the object's department and the request occurs during business hours.
Example Attributes
-
User attributes: department, clearance, role, device security level
-
Object attributes: classification, owner, project
-
Environmental attributes: time, location, authentication strength
ABAC policies are evaluated and enforced by distributed policy engines, often across multiple systems in large-scale environments.
They allow dynamic conditions but can be complex to test and manage.
Cloud platforms such as AWS IAM and Google Cloud use ABAC-style rules to refine role-based access.
Weaknesses: ABAC offers flexibility but poor transparency.
Policies may interact in unexpected ways, and administrators may find it difficult to reason about all possible conditions.
The Chinese Wall Model
The Chinese Wall model handles conflicts of interest in consulting, law, and finance.
Access depends on a user’s history, not just attributes or roles.
Once a user accesses data for one client, the system prevents access to competing clients’ data within the same conflict class.
For example, a consulting firm may define:
-
Beverage class: {Coca-Cola, PepsiCo, Keurig Dr Pepper}
-
Airline class: {United, Delta, JetBlue, American Airlines, Alaska Airlines}
An analyst who views Coca-Cola’s data can no longer view PepsiCo’s or Keurig Dr Pepper’s confidential data, but can still access airline clients.
This dynamic restriction enforces need-to-know and prevents cross-client information flow.
The model is a dynamic form of separation of duties, where restrictions depend on a user’s previous actions rather than static role definitions.
Sanitizing Data
In practice, sanitized or anonymized information may be shared across conflict classes without violating the Chinese Wall policy.
Aggregate statistics or generalized insights can be used safely once identifying details are removed, allowing useful analysis while maintaining confidentiality.
Weaknesses: It requires accurate tracking of user activity and careful definition of conflict classes. Overly broad classes can block legitimate work; overly narrow ones may miss conflicts.
Comparative Overview
Access control models evolved to address different needs and trade-offs.
| Model | Main Focus | Policy Defined By | Dynamics | Strength | Weakness |
|---|---|---|---|---|---|
| DAC | Ownership and sharing | User | Static | Simple and flexible | No containment; user-dependent |
| MAC | Confidentiality and integrity | System | Static | Strong enforcement | Complex and inflexible |
| RBAC | Job functions | Administrator | Limited | Scalable and structured | Static; prone to role explosion |
| ABAC | Context and attributes | Policy engine | Dynamic | Flexible and context-aware | Hard to audit and manage |
| Chinese Wall | Conflict of interest | System (history-based) | Dynamic | Enforces ethical boundaries | Administratively complex |
Key Takeaways
-
DAC allows user-managed sharing but offers weak containment.
-
MAC enforces strong, system-defined confidentiality and integrity rules but is hard to manage.
-
RBAC organizes permissions by job function for scalable administration.
-
ABAC adds flexibility through attribute-based, context-aware decisions.
-
Chinese Wall prevents conflicts of interest by making access depend on prior actions.
-
The principle of least privilege and privilege separation apply across all models to minimize risk.
-
Real systems combine multiple models to balance usability, assurance, and administrative effort.
Week 7: Memory Vulnerabilities and Defenses
Understanding memory vulnerabilities is essential to understanding how systems fail and how they are defended. We want to understand how memory errors arise, how they are exploited, and how modern systems defend against them.
Memory Corruption and Exploitation
Most software vulnerabilities stem from incorrect handling of memory. In C and C++, the compiler trusts the programmer to manage memory correctly. When programs read or write beyond valid memory boundaries, they corrupt nearby data and sometimes control structures. These problems lead to memory corruption, the root cause of buffer overflows, integer overflows, and use-after-free bugs.
Process Memory Layout
Every running program occupies virtual memory organized into distinct regions. Understanding this layout is important because vulnerabilities arise from how programs use these regions incorrectly:
-
Text (code): Contains the compiled machine instructions. This region is marked read-only and executable to prevent modification.
-
Data and BSS: Store global and static variables, either initialized (data) or zero-initialized (BSS).
-
Heap: Holds dynamically allocated memory from
malloc(),calloc(), ornew. The heap grows upward toward higher addresses as new allocations are made. -
Shared libraries: Mapped into the process address space, providing reusable code and data.
-
Stack: Stores temporary data for function calls, including parameters, return addresses, and local variables. The stack grows downward toward lower addresses with each new function call.
-
Kernel space: Reserved for the operating system, separated from user-space memory.
This layout is consistent across most UNIX, Linux, and Windows systems, though exact addresses vary.
Because the address space on current processors is huge, systems leave unmapped memory between these regions as guard zones. An overflow that extends beyond its region will typically hit unmapped memory and trigger a segmentation fault before reaching another valid region.
Stack Buffer Overflows
A buffer overflow happens when data exceeds the size of a destination buffer. Stack buffer overflows are particularly dangerous because of how the stack organizes function data.
When a function is called, the compiler creates a stack frame containing the function's parameters, the return address (where execution should resume after the function completes), saved registers, and local variables. The stack grows downward in memory: each new function call creates a frame at a lower memory address than the previous one. Within a frame, local variables are typically stored at lower addresses than the saved return address.
This arrangement creates a critical vulnerability. When a buffer overflow occurs in a local variable, the excess data writes into memory at higher addresses, moving "up" through the stack frame. If the overflow is large enough, it overwrites other local variables first, then the return address itself. When the function attempts to return, the CPU pops the corrupted return address from the stack and jumps to it. If an attacker controls the overflow data, they can redirect execution to attacker-chosen code.
Consider a local character array of 16 bytes. If an unsafe function like gets() copies 100 bytes into this array, the excess 84 bytes overwrite whatever happens to be stored in the higher addresses of the stack frame. The attack succeeds because the stack holds both user data (the buffer) and control data (the return address) in adjacent memory, with no enforced boundary between them.
Heap Buffer Overflows
Heap buffer overflows work differently because heap memory has a different structure. When malloc() allocates memory, the allocator maintains metadata adjacent to each allocated block. This metadata includes the size of the block, status flags, and pointers that link free blocks together in the allocator's free lists.
A heap overflow occurs when data written to an allocated buffer extends past its boundary into adjacent memory. Unlike stack overflows that target return addresses, heap overflows typically corrupt either the metadata of neighboring blocks or the contents of adjacent allocations. If the overflow corrupts allocator metadata, subsequent malloc() or free() operations may behave unpredictably, potentially allowing an attacker to write arbitrary values to chosen memory locations. If the overflow corrupts a neighboring allocation, the attacker may overwrite application data structures, including function pointers or object vtables that control program behavior.
Integer overflows are subtler. Arithmetic that exceeds the maximum value of a type wraps around to zero. A calculation that allocates too small a buffer, for example, can make a later copy operation overwrite adjacent memory. Off-by-one errors fall into the same category: a loop that copies one extra byte can overwrite a boundary value such as a null terminator or a saved pointer.
Use-after-free bugs occur when a program continues to use memory after freeing it. The physical memory still exists and may contain old data, but the allocator can reuse it at any time. If the allocator reuses that memory for another object, the program may dereference stale pointers that now point to attacker-controlled data. Attackers exploit this through heap spraying: filling the heap with controlled data so that freed memory likely contains attacker values when accessed. This can redirect function pointers or vtable pointers to attacker-controlled addresses.
Double-free bugs occur when the same memory is freed twice. This corrupts the allocator's internal free lists, which link available chunks using pointers stored in the freed memory itself. After a double-free, the allocator may return the same memory address from two separate malloc() calls, breaking memory isolation. Attackers can manipulate free-list pointers to force the allocator to return pointers to arbitrary memory locations.
Format-string vulnerabilities appear when untrusted input is used directly as a format argument to printf or similar functions. Directives such as %x print data from the stack, and %n writes a value to a memory address that is read from the stack. If the format string comes from user input, the attacker can read memory or even write arbitrary values to attacker-chosen locations.
Early exploits injected shellcode, machine instructions placed into a writable buffer, and redirected execution to run them. When systems began marking writable pages as non-executable, attackers adapted their techniques to work within these new constraints.
Defensive Mechanisms
Each defensive measure was developed to close a gap that earlier systems left open. Together, they form the layered protection that modern systems rely on.
Non-executable memory (NX, DEP, W^X)
The first step was to separate code from data. NX (No eXecute) or DEP (Data Execution Prevention) marks writable memory as non-executable. This capability is provided by the processor's memory management unit (MMU) and configured by the operating system when it sets up page permissions. The CPU refuses to execute any instructions from pages marked non-executable, preventing injected shellcode from running. NX does not stop memory corruption itself, but it eliminates the simplest outcome: running arbitrary injected code.
Adapting to non-executable memory
When NX made shellcode injection impractical, attackers shifted to code reuse techniques. These approaches work because they execute only code that is already marked executable: they simply chain it together in ways the original programmers never intended.
Return-to-libc was the first widely used code reuse technique. Instead of injecting shellcode, an attacker overwrites a return address to point to an existing library function such as system(). By carefully arranging data on the stack, the attacker can make that function execute with attacker-chosen arguments. For example, redirecting to system("/bin/sh") spawns a shell without injecting any code.
Return-to-libc works because library functions are already executable. The attack reuses trusted code for untrusted purposes. The main limitation is that the attacker must find a single function that accomplishes their goal and must be able to set up its arguments correctly.
Return-oriented programming (ROP) generalizes this idea. Instead of calling a single function, ROP chains together short sequences of instructions called gadgets. Each gadget is a fragment of existing code that ends with a return instruction. By placing a sequence of gadget addresses on the stack, an attacker can compose arbitrary computation from these fragments.
ROP works because each gadget ends with a return, which pops the next address from the stack and jumps there. The attacker controls what addresses are on the stack, effectively writing a program out of pre-existing instruction sequences. With enough gadgets, an attacker can perform any operation (load values, perform arithmetic, make system calls) all without injecting a single byte of code.
ROP is more powerful than return-to-libc but also more complex. The attacker must find suitable gadgets in the executable memory of the target process and must know their addresses. This requirement explains why address randomization (ASLR) is so important: it makes gadget locations unpredictable.
Address-space layout randomization (ASLR)
Return-to-libc and ROP showed that NX alone was not enough. Attackers could still call existing functions or chain gadgets if they knew their addresses. ASLR fixed that by randomizing the layout of the process address space. Each run places the stack, heap, and libraries at unpredictable locations. Without that knowledge, hardcoded addresses no longer work reliably. ASLR's strength depends on the randomness available and on the absence of information leaks that reveal memory addresses.
Stack canaries
Stack canaries add a small random value between local variables and saved control data on the stack. The program checks the value before returning from a function. If the canary changed, execution stops. This defense detects stack overflows that overwrite return addresses, preventing direct control hijacking. The idea is simple but powerful: any corruption that changes the control data must also change the canary.
Heap canaries and allocator hardening
Heap corruption exploits were once as common as stack overflows. Modern allocators introduced defenses modeled after stack canaries and added several more.
Heap blocks may include heap canaries (or cookies): small guard values placed before or after each block's user data. When a block is freed, the allocator verifies that the guard is intact. If an overflow or underflow modified it, the program aborts.
Allocators also use safe unlinking to validate free-list pointers, pointer mangling to encode metadata with a secret, and quarantining to delay reuse of freed blocks. Quarantining prevents use-after-free exploitation by holding freed memory in a queue before making it available for reuse. Double-free detection tracks whether chunks are currently free and aborts if the same memory is freed twice. These techniques make heap corruption much less predictable and far harder to exploit.
Safer libraries and compiler checks
Many vulnerabilities arise from unsafe standard functions such as gets, strcpy, or sprintf, which do not enforce buffer limits. Modern compilers and libraries address this by warning developers or substituting safer variants like fgets, strncpy, and snprintf. Options such as FORTIFY_SOURCE in gcc can perform runtime checks to detect certain unsafe copies. The goal is to eliminate the easy mistakes that once led to catastrophic failures.
Linker and loader hardening
Dynamic linking once allowed attackers to tamper with relocation tables and redirect function calls. Linker and loader hardening, such as RELRO (RELocation Read-Only), marks these tables read-only after initialization and resolves symbols early. This removes the possibility of overwriting linkage data to redirect control flow.
Development-time Protections
Preventing memory vulnerabilities during development is more effective than mitigating them at runtime. Modern testing tools make many memory bugs visible before deployment.
Compiler instrumentation can add runtime checks to detect invalid memory operations and arithmetic errors during testing. An AddressSanitizer (ASan) is the most widely used tool: it detects buffer overflows, use-after-free, and double-free by maintaining shadow memory that tracks the state of every byte. When memory is freed, ASan marks it as invalid and quarantines it to increase the detection window. These checks turn silent corruption into clear, reproducible failures. These features are used only during development because they slow execution (2-3x overhead), but they find the same classes of vulnerabilities that attackers exploit.
Fuzzing complements compiler instrumentation by generating a large number of random or mutated inputs, watching for crashes and test failures. Coverage-guided fuzzers automatically explore new code paths and expose edge cases that human testing might never reach. Fuzzing does not prove correctness;it simply finds the conditions that lead to failure. Combined with compiler instrumentation, it is one of the most effective ways to uncover memory-safety bugs before software is released.
Together, these testing tools address the visibility problem: they make hidden memory errors observable and fixable long before deployment.
Hardware Mechanisms
Modern processors now assist in enforcing memory safety directly.
Control-flow integrity (CFI). Hardware support such as Intel's Control-flow Enforcement Technology (CET) protects return addresses and indirect branches. A shadow stack stores a verified copy of each return address, detecting tampering. Indirect branch tracking ensures jumps go only to legitimate targets.
Pointer authentication. Some architectures add a short integrity check to pointer values so the processor can detect when a pointer has been modified. This prevents forged return addresses or corrupted function pointers from being used.
Memory tagging. Hardware features like ARM's Memory Tagging Extension (MTE) associate small tags (4 bits) with memory allocations and pointers. When memory is freed, its tag changes. Any subsequent access through a pointer with the old tag triggers an exception. The processor checks tags on each access, revealing use-after-free and out-of-bounds errors with minimal performance cost (<5% overhead). These features extend the same principle as software defenses (detect corruption and verify integrity) but enforce it in hardware.
How the Layers Work Together
Memory protection is not one mechanism but a collaboration across the system.
-
Hardware enforces basic integrity and blocks unsafe control transfers.
-
The operating system controls memory permissions and layout.
-
The compiler inserts runtime checks and promotes safer APIs.
-
Development tools detect vulnerabilities before software ships.
Each layer covers weaknesses the others cannot. NX stops shellcode. ASLR hides addresses. Canaries detect overwrites. Allocator hardening prevents metadata abuse. Hardware features validate control flow. Testing tools find the bugs that remain. No single technique provides security, but together they make exploitation unreliable and expensive.
Main points
-
Memory-safety problems arise from trusting memory contents that can change unexpectedly - usually due to code that either does not check for buffer limits or sets limits improperly due do integer overflow.
-
Each defense was designed to close a specific gap: NX for code injection, ASLR for address predictability, canaries for overwrites, allocator hardening for heap corruption and double-free, and quarantining for use-after-free.
-
Code reuse techniques (return-to-libc and ROP) emerged as responses to non-executable memory protections.
-
Hardware integrity checks and tagging extend these protections into the processor.
-
Compiler instrumentation and fuzzing expose vulnerabilities before deployment.
-
Defense-in-depth, not perfection, is the goal: multiple layers of modest protection combine to make attacks impractical.
Week 8: Command Injection and Input Validation Attacks
Command injection attacks exploit how programs interpret user input as executable commands rather than as data. They differ from memory corruption: the attacker alters what command runs instead of what code runs. These attacks affect databases, shells, file systems, and development environments, and remain among the most persistent classes of software vulnerabilities.
SQL Injection
SQL injection manipulates database queries by embedding SQL syntax in user input. It can expose, alter, or delete data and even execute administrative commands.
Primary Defenses
The core defense is to keep query structure fixed and pass data separately through:
-
Parameterized queries: Database receives commands and data separately, never interpreting data as SQL syntax
-
Stored procedures: Predefined SQL functions with fixed structure that accept only data parameters
Secondary Defense
Input validation and sanitization add a second layer but cannot be relied on alone. Use allowlists that specify what characters are permitted, not denylists that try to block dangerous patterns. Sanitization through escaping special characters (e.g., using database-specific escaping functions) can help but is error-prone and should never replace parameterized queries.
NoSQL Injection
NoSQL databases avoid SQL syntax but still parse user input that can include operators or code. Injection can happen when JSON or query operators are accepted unchecked.
Defense principles:
-
Validate input types (expect strings, reject objects)
-
Restrict operator usage (block
$where,$regex, and other dangerous operators) -
Avoid execution of user-supplied code such as JavaScript in
$whereclauses -
Use schema enforcement and allowlists to reduce exposure
Shell Command Injection
Shell injection exploits programs that pass user input to command interpreters like sh, bash, or cmd.exe. Shell metacharacters (;, |, $(), backticks) enable attackers to append new commands or substitute results.
Safest defense: Avoid shells entirely and use system APIs that execute programs directly, passing arguments as separate parameters (e.g., execve() with argument array, Python's subprocess with shell=False).
When shell use is unavoidable, combine allowlist validation with proper sanitization (e.g., shlex.quote() in Python to escape shell metacharacters), and run the process with minimal privileges.
Environment Variable Attacks
Programs inherit environment variables that control their behavior. These can be exploited through two distinct attack vectors.
Command Resolution Attacks (PATH, ENV, BASH_ENV)
Attack mechanism: Control which executable runs when a program or script invokes a command by name.
PATH manipulation redirects command lookups by placing attacker-controlled directories early in the search path. When a script runs ls or wget, the shell searches PATH directories in order. An attacker who can modify PATH or write to an early PATH directory can substitute malicious executables.
ENV and BASH_ENV specify initialization scripts that run when shells start. If controlled by an attacker, these variables cause arbitrary commands to execute at the beginning of every shell script, affecting system scripts and cron jobs.
Defenses:
-
Scripts with elevated privileges should explicitly set PATH to trusted directories
-
Unset ENV and BASH_ENV in security-sensitive contexts
-
Use absolute paths for critical commands (e.g.,
/usr/bin/lsinstead ofls)
Library Loading Attacks (LD_PRELOAD, LD_LIBRARY_PATH, DLL Sideloading)
Attack mechanism: Control which shared libraries are loaded into running programs, allowing function-level hijacking rather than executable replacement.
LD_PRELOAD (Linux/Unix) specifies libraries to load before all others, enabling attackers to override standard library functions like malloc(), read(), or rand(). Through function interposition, the attacker's replacement function can call the original after modifying parameters or logging data - making attacks stealthy since the program continues to work normally while being monitored or manipulated.
LD_LIBRARY_PATH (Linux/Unix) redirects library searches to attacker-controlled directories before system directories.
DLL sideloading (Windows) exploits the DLL search order. Windows searches the executable's directory before system directories, allowing attackers to place malicious DLLs that will be loaded instead of legitimate system libraries.
Why library loading attacks are distinct:
-
More surgical: Override individual functions, not entire programs
-
More powerful: Hijack cryptographic functions, logging, authentication checks
-
Function interposition: Wrap original functions to add malicious behavior while maintaining normal operation (e.g., log all writes while still writing to files)
-
Different protection: Operating systems block LD_PRELOAD/LD_LIBRARY_PATH in setuid programs, but user-level attacks remain effective
-
Cross-platform parallel: LD_PRELOAD on Unix maps directly to DLL sideloading on Windows
Defenses:
-
Specify full paths when loading libraries
-
Verify library authenticity through digital signatures
-
Use Secure DLL Search Mode on Windows
-
Privileged applications should sanitize or ignore library-related environment variables
-
Note: Even with OS protections for privileged programs, attacks on user-level programs remain practical
Package and Dependency Attacks
Modern software depends heavily on third-party packages. Attackers exploit this through typosquatting (packages with names similar to popular ones), dependency confusion (preferring public packages over internal ones), and malicious installation scripts.
These are supply chain attacks rather than direct code injection but have the same effect: untrusted code executes with developer privileges. They represent command injection at build time—they exploit the same trust failure but target development environments instead of running applications.
Path Traversal
Path traversal occurs when user input controls file paths and uses relative path elements (..) to escape restricted directories. Attackers may exploit symbolic links, encoding tricks, or platform differences to bypass filters.
Path equivalence is a related vulnerability where multiple different path strings can reference the same file or directory. Operating systems and file systems may treat paths as equivalent even when they differ textually. Examples include: redundant slashes (///file vs /file), alternative separators (\ vs / on Windows), case variations on case-insensitive systems, or mixed use of . (current directory). Attackers exploit path equivalence to bypass validation that checks for exact string matches, allowing access to restricted resources through alternate representations.
Defenses:
-
Resolve paths to absolute form before validation (canonicalization)
-
Avoid direct concatenation of user input into file paths
-
Restrict application permissions so that even successful traversal yields limited access
Path traversal and character encoding attacks often overlap. Both exploit how systems interpret or normalize input paths, and both are prevented by consistent canonicalization—resolving paths and encodings to a standard form before applying security checks.
Character Encoding Issues
Encoding attacks rely on multiple representations of the same character to bypass validation. Overlong UTF-8 encodings and nested URL encodings can slip through checks that decode input later.
General rule: Decode and normalize before validating. Applications should reject ambiguous encodings and rely on standard, well-tested parsing libraries rather than custom decoders.
Race Conditions (TOCTTOU)
A time-of-check to time-of-use (TOCTTOU) vulnerability arises when a resource changes between validation and use. This can allow an attacker to substitute a protected file or link after a permissions check.
Fixes:
-
Use atomic operations that check and act in one step
-
Manipulate open file descriptors rather than filenames
-
Avoid separate "check then act" logic whenever possible
File Descriptor Misuse
Programs often assume that standard input, output, and error descriptors (0, 1, 2) are valid. If an attacker closes these before running a privileged program, new files may reuse those descriptor numbers. Output intended for the terminal may overwrite sensitive files.
Defense: Secure programs verify and reopen descriptors 0–2 before performing any file operations.
Input Validation
Input validation underpins all injection defenses but is difficult to implement correctly.
Validation Approaches
- Allowlisting (safest)
- Specify what is allowed. Accept only characters, patterns, or values that are explicitly permitted. Unknown inputs are rejected by default.
- Denylisting (less safe)
- Specify what is forbidden. Reject input containing dangerous patterns. Attackers often find bypasses through creative encodings or edge cases.
Sanitization Techniques
When potentially dangerous input must be processed, sanitization modifies it to make it safe:
- Escaping special characters
- Add escape sequences to neutralize characters with special meaning in the target context (SQL, shell, etc.). Use established libraries like Python's
shlex.quote()for shell commands rather than manual escaping. - Removing or replacing characters
- Strip out or substitute dangerous characters entirely. This is simpler than escaping but may be too restrictive for legitimate input.
Important: Sanitization should be context-specific and used as a secondary defense alongside proper APIs that separate commands from data.
Key Principles
-
Validate after decoding and normalization
-
Consider how the input will be used (context matters)
-
Length limits alone do not prevent injection
-
Use safe APIs that separate commands from data
-
Minimize trust boundaries in the system
Comprehension and Design Errors
Most injection flaws result from misunderstandings: programmers don't fully grasp how interpreters parse input or how system calls behave.
Common misunderstandings:
-
Not knowing all special characters that need escaping
-
Not realizing standard file descriptors can be closed
-
Assuming filenames don't contain special characters
-
Not understanding URL decoding order
-
Believing a simple string search prevents path traversal
-
Thinking validation and escaping are equivalent
Reducing errors:
-
Prefer simple, safe APIs that are hard to misuse
-
Provide secure examples in documentation
-
Make insecure options hard to reach (secure defaults)
-
Education and code review focused on platform-specific quirks
Defense in Depth
No single control can prevent all injection vulnerabilities. Secure systems rely on multiple layers:
-
Validate input at boundaries using allowlists where possible
-
Use APIs that isolate data from code (parameterized queries, argument arrays)
-
Run with least privilege and sandbox where possible
-
Audit and test for injection behaviors through code review and penetration testing
-
Monitor for suspicious activity through logging and anomaly detection
Command and input injection attacks persist because they exploit human assumptions about how software interprets input. Understanding those interpretations -- and designing systems that never blur data and commands -- is essential for secure programming.
Week 8: Containment and Application Isolation
Containment limits what a compromised process can do after an attack succeeds. Even with proper input validation, vulnerabilities may remain, and if an attacker gains control of a process, traditional access controls become ineffective since the operating system assumes the process acts within its assigned privileges. Containment creates isolation boundaries that confine the impact of a faulty or malicious program, preventing it from affecting the rest of the system.
Containment operates at multiple layers:
-
Application sandboxes restrict individual processes
-
Containers isolate sets of processes
-
Virtual machines emulate entire operating systems
-
Hardware-based isolation enforces security boundaries below the operating system level
Application Sandboxing
A sandbox is a restricted execution environment that mediates interactions between an application and the operating system by limiting resource access, system calls, and visible state. Sandboxing evolved from early filesystem-based confinement to kernel-level and language-level environments that can restrict both native and interpreted code.
Filesystem-Based Containment
chroot
The chroot system call changes a process's view of the root directory to a specified path, so all absolute paths are resolved relative to that new root. Child processes inherit this environment, creating a chroot jail. This mechanism affects only the filesystem namespace and does not restrict privileges or system calls.
A process with root privileges inside a chroot jail can escape by:
-
Manipulating directory structures (creating a subdirectory, chrooting into it, then traversing upward)
-
Using
ptraceto attach to processes outside the jail if accessible -
Creating device nodes to access system memory or disk directly
The chroot mechanism provides no limits on CPU, memory, or I/O usage and requires copying all dependent executables, libraries, and configuration files into the jail. While still used for testing or packaging, chroot is not suitable for reliable containment.
FreeBSD Jails
FreeBSD Jails extended chroot by adding process and network restrictions, but still lacked fine-grained resource management.
System Call-Based Sandboxes
The system call interface defines the actual power of a process, as every interaction with resources goes through a system call. A system call sandbox intercepts calls and applies a policy before allowing execution, with enforcement occurring either in user space or within the kernel.
User-Level Interposition
Early implementations operated entirely in user space, often using the ptrace debugging interface to monitor processes. Janus (UC Berkeley) and Systrace (OpenBSD) are examples of this approach that relied on user-level processes for policy enforcement. Each system call was intercepted and checked against a policy before being allowed or denied. A policy might allow file access under a specific directory but deny network activity.
This approach had significant weaknesses:
-
Race conditions could occur between the check and the actual call (time-of-check-time-of-use vulnerabilities). A program could pass a safe filename during the check, then quickly change it to a sensitive file before execution.
-
Tracking all side effects of system calls was challenging (operations on file descriptors, file descriptor assignment and duplication, relative pathname parsing)
-
Multithreaded programs could bypass monitoring
-
Each system call introduced substantial overhead from context switches to the tracer
User-level interposition demonstrated feasibility but was not robust enough for production use.
Kernel-Integrated Filtering: seccomp-BPF
Linux moved sandbox enforcement into the kernel with Secure Computing Mode (seccomp). Modern systems use seccomp-BPF, which adds programmable filtering through BPF bytecode. The process installs a filter that the kernel executes whenever it attempts a system call, inspecting the system call number and arguments and returning actions such as:
-
SECCOMP_RET_ALLOW: permit the call -
SECCOMP_RET_ERRNO: block with error -
SECCOMP_RET_TRAP: deliver a signal -
SECCOMP_RET_KILL: terminate the process
Once installed, filters cannot be relaxed—only replaced with stricter ones.
Advantages:
-
Enforcement in the kernel eliminates race conditions
-
Fine-grained control over allowed calls and arguments
-
Low runtime overhead compared to user-space approaches
Limitations:
-
Policies are static and written in low-level BPF syntax
-
Does not manage resources or filesystem visibility
Seccomp-BPF is now widely used in browsers, container runtimes, and service managers to reduce kernel attack surfaces.
AppArmor
While seccomp-BPF provides powerful system call filtering, it cannot inspect pathnames passed as arguments to system calls. For example, it can allow or deny the open() system call entirely, but cannot distinguish between opening /etc/passwd versus /tmp/file. This limitation exists because seccomp-BPF operates at the system call interface and can only examine raw arguments like file descriptors and memory addresses, not the filesystem paths they reference.
AppArmor addresses this gap by enforcing Mandatory Access Control (MAC) policies based on pathnames. It operates as a Linux Security Module (LSM) in the kernel and mediates access to files and directories by checking the requested path against a per-program security profile. An AppArmor profile can specify rules like "allow read access to /var/www/**" or "deny write access to /etc/**."
AppArmor complements seccomp-BPF: seccomp-BPF restricts which system calls a process can make, while AppArmor restricts which resources those calls can access. Together, they provide defense in depth—one limiting the interface to the kernel, the other limiting access to specific objects within the filesystem namespace.
Language-Based Sandboxing
Some sandboxes operate entirely in user space by running code inside managed execution environments called process virtual machines. These environments provide language-level isolation by interpreting or compiling bytecode to a restricted instruction set.
Common examples include:
-
Java Virtual Machine (JVM): Verifies bytecode before execution, ensuring operations stay within defined type and memory bounds
-
Microsoft .NET Common Language Runtime (CLR): Provides managed execution for C#, VB.NET, and other languages
-
Python interpreter: Can confine execution by controlling access to modules
-
JavaScript engines: Browser engines restrict access to filesystem and network, allowing only specific APIs
These environments emulate a CPU and manage memory internally. Programs run as bytecode (which may be interpreted or compiled just-in-time) and cannot directly access hardware or invoke system calls. All external interaction goes through controlled APIs.
Strengths:
-
Memory safety and portability across platforms
-
No direct system calls
-
Logical separation between user code and host resources
Limitations:
-
Depend on runtime correctness—a flaw in the interpreter breaks isolation
-
Limited ability to enforce fine-grained resource policies
-
The runtime itself must be sandboxed at the OS level
Language-based sandboxes often coexist with kernel-level sandboxes. For instance, a web browser runs JavaScript inside an interpreter sandbox while using seccomp or Seatbelt to confine the browser process itself.
Sandbox Evolution
Application sandboxing evolved from restricting what a process can see to restricting what it can do:
-
Filesystem-based approaches like chroot provided simple legacy compatibility but no control of system calls or privileges
-
System call-based sandboxes at the kernel level offer fine-grained and efficient control but require complex or static configuration
-
Language-based sandboxes provide memory-safe and portable environments but depend on runtime integrity
OS-Level Isolation Primitives
System call sandboxes confine individual processes, but most applications consist of multiple cooperating processes. To contain such systems, the operating system must isolate groups of processes and the resources they share. Linux provides three kernel mechanisms for this purpose:
-
Namespaces: Define which resources a process can see
-
Control groups (cgroups): Define how much of each resource a process can use
-
Capabilities: Define what privileged actions a process may perform
Together, these mechanisms form the foundation for containers.
Namespaces
A namespace gives a process its own private copy of part of the system's global state. Processes that share a namespace see the same view of that resource, while those in different namespaces see distinct views. Each namespace type isolates one kernel subsystem.
Linux supports several namespace types:
-
PID namespaces: Isolate process IDs so each namespace has its own PID 1; processes cannot see or signal those in other namespaces
-
Mount namespaces: Allow each to mount or unmount filesystems independently
-
UTS namespaces: Isolate hostname and domain name
-
Network namespaces: Provide private network stacks with their own interfaces, routing tables, and sockets
-
IPC namespaces: Isolate System V and POSIX IPC objects like shared memory or semaphores
-
User namespaces: Map internal UIDs to different real UIDs on the host
-
Cgroup namespaces: Control visibility of control-group resources
Each namespace acts like a self-contained copy of a subsystem. Namespaces let multiple isolated environments run on a single kernel, providing the illusion of separate systems without hardware virtualization. However, they hide and partition resources but do not limit consumption.
Control Groups (cgroups)
A control group (cgroup) manages and limits resource usage. While namespaces define what a process can see, cgroups define how much of each resource it can use. A cgroup is a hierarchy of processes with limits on resource usage, where each type of resource is managed by a controller that measures consumption and enforces restrictions.
Common controllers manage:
-
CPU: Scheduling and quotas
-
Memory: Physical and swap memory limits
-
PIDs: Process count limits to prevent fork bombs
A service can belong to several cgroups with different controllers. The kernel tracks usage per group and enforces limits through scheduling and memory reclamation. If a process exceeds its memory quota, the kernel's out-of-memory (OOM) handler terminates it without affecting other groups.
Namespaces and cgroups together isolate processes functionally and economically: each process group sees only its own resources and consumes only what it is permitted.
Capabilities
Traditional Unix privilege management treated the root user (UID 0) as all-powerful, checking only whether the process's effective user ID was zero. This binary model violated the principle of least privilege.
Capability Model
Capabilities break up root's privilege into specific pieces. The kernel no longer assumes that UID 0 can do everything by default; each privileged operation now requires the matching capability. Each capability represents authorization for a specific class of privileged operation, such as configuring network interfaces (CAP_NET_ADMIN) or loading kernel modules (CAP_SYS_MODULE). Under this model, UID 0 alone no longer implies complete control—the kernel checks both the user ID and capability bits before allowing any privileged action.
Common Capabilities
Linux defines over 40 distinct capabilities. Some important examples include:
-
CAP_NET_ADMIN: Modify network configuration
-
CAP_SYS_MODULE: Load and unload kernel modules
-
CAP_SYS_TIME: Change the system clock
-
CAP_NET_BIND_SERVICE: Bind to privileged ports (below 1024)
-
CAP_DAC_OVERRIDE: Bypass file permission checks
For instance, a web server can be granted only CAP_NET_BIND_SERVICE to bind to port 80 while running as a non-root user. Even if compromised, it cannot mount filesystems, modify network routing, or change the system clock.
Applying Capabilities
Capabilities can be attached to executable files or granted to running processes. Once dropped, capabilities cannot be regained unless the process executes another binary that has them defined. Entering a user namespace alters capability behavior—a process can appear to be root inside the namespace, but its capabilities apply only within that namespace, not to the host.
Root Under Capabilities
A process with UID 0 must still have the appropriate capabilities to perform privileged operations; the UID alone is not sufficient. A non-root process given a specific capability can perform only the operation covered by that capability. Processes can permanently relinquish capabilities, allowing them to perform initialization requiring privilege and then continue safely with minimal rights, implementing the principle of least privilege.
Integration
-
Namespaces isolate visibility by giving each process its own view of system resources
-
Control groups enforce limits on resource consumption
-
Capabilities break up root privilege into narrowly scoped rights
Together, these mechanisms implement the principle of least privilege at the operating-system level, restricting what a process can see, what it can consume, and what it can do.
Containerization
Containerization builds on namespaces, control groups, and capabilities to package applications and their dependencies into lightweight, portable units that behave like independent systems. Each container has its own processes, filesystem, network interfaces, and resource limits, yet all containers run as ordinary processes under the same kernel.
Purpose and Design
Containers were introduced primarily to simplify the packaging, deployment, and distribution of software services. They made it possible to bundle an application and its dependencies into a single, portable image that could run the same way in development, testing, and production. The underlying mechanisms were developed for resource management and process control, not for security. As container frameworks matured, these same mechanisms also provided practical isolation, making containers useful for separating services, though not as a strong security boundary.
Container Operation
Traditional virtualization runs multiple operating systems by emulating hardware, with each virtual machine including its own kernel and system libraries. This offers strong isolation but duplicates system components, consuming memory and startup time. Containers achieve similar separation with less overhead by virtualizing the operating system interface—the process and resource view provided by the kernel—rather than hardware.
How the three mechanisms combine in containers:
-
Namespaces give each container its own process IDs, network stack, hostname, and filesystem view
-
Cgroups limit how much CPU time, memory, and disk bandwidth each container can consume
-
Capabilities restrict privileged operations so that even root inside a container is not root on the host
This layered design allows thousands of isolated services to run on one host without the duplication inherent in full virtual machines.
How Containers Work
Containers are a structured way to combine kernel features into a managed runtime. Each container starts as an ordinary process, but the container runtime (such as Docker, containerd, or LXC) configures it with:
-
New namespaces for isolated process IDs, network stack, hostname, and filesystem
-
Cgroups that define resource limits
-
Restricted capabilities so even root inside the container has limited privileges
-
A filesystem built from an image—a prebuilt snapshot containing all files, libraries, and configuration
Container runtimes automate the setup of kernel mechanisms and apply consistent, minimal-privilege defaults. Images are layered and can be stored in registries, making it easy to distribute and deploy applications consistently across different environments. This combination of isolation, resource control, and portability is why containers became central to modern software deployment.
Security Characteristics
Containers improve isolation but do not create a full security boundary. All containers share the same kernel, so a vulnerability in the kernel could allow one container to affect others. Within a container, the root user has administrative control inside that namespace but not on the host. However, kernel bugs or misconfigured capabilities can weaken that boundary.
To strengthen isolation, systems often combine containers with additional mechanisms:
-
seccomp-BPF filters block dangerous system calls
-
Mandatory Access Control (MAC) frameworks like SELinux or AppArmor restrict filesystem and process access
-
Running containers inside virtual machines for an extra hardware barrier
Containers provide meaningful isolation for ordinary services but are not appropriate for untrusted or hostile code without additional containment layers.
Practical Benefits
Beyond isolation, containers provide significant advantages:
-
Portability: Applications run the same way in development, testing, and production because each container includes its dependencies
-
Efficiency: Containers start quickly and use fewer resources than virtual machines
-
Density: Many containers can share a single kernel, allowing high utilization of servers
-
Manageability: Tools automate deployment, scaling, and monitoring
The same kernel features that provide containment also make containers predictable to manage and easy to orchestrate at scale.
Virtualization
Virtualization moves the boundary of isolation to the hardware level. A virtual machine (VM) emulates an entire computer system including CPU, memory, storage, and network interfaces. Each VM runs its own operating system and kernel, independent of the host. From the guest operating system's perspective, it has full control of the hardware, even though that hardware is simulated. This approach provides strong isolation because the guest cannot directly access the host's memory or devices.
Virtualization Mechanics
Virtualization creates the illusion that each operating system has exclusive access to the hardware. A software layer called a hypervisor or Virtual Machine Monitor (VMM) sits between the hardware and the guest operating systems. It intercepts privileged operations, manages memory and device access, and schedules CPU time among the guests.
When a guest operating system issues an instruction that would normally access hardware directly, the hypervisor traps that instruction, performs it safely on the guest's behalf, and returns the result. With modern hardware support, most instructions run directly on the CPU, with the hypervisor only intervening for privileged operations. This allows near-native performance while maintaining separation between guests.
Modern processors include hardware support for virtualization, allowing the CPU to switch quickly between executing guest code and hypervisor code, reducing overhead.
Hypervisor Types
Type 1 (bare-metal) hypervisors run directly on hardware and manage guest operating systems, with the hypervisor effectively serving as the host OS. They are more efficient and used in data centers and clouds.
Type 2 (hosted) hypervisors run as applications under a conventional operating system and use that OS's device drivers. They are easier to install on desktop systems and used for testing, development, or running alternative OSes.
Containers vs. Virtual Machines
A container isolates processes but shares the host kernel. A virtual machine isolates an entire operating system with its own kernel. This key difference means:
-
VMs are more secure (stronger isolation, each has its own kernel) but heavier (duplicate OS components, slower startup)
-
Containers are more efficient (share kernel, fast startup, low overhead) but provide weaker isolation (kernel vulnerabilities affect all containers)
VMs can run different operating systems simultaneously; containers must use the host kernel. In practice, many systems combine both: running containers inside VMs to balance efficiency with strong isolation.
Virtualization Advantages
-
Strong isolation: Each guest runs in its own protected memory space and cannot interfere with others
-
Hardware independence: The hypervisor emulates a uniform hardware interface, allowing guests to run on different physical machines without modification
-
Snapshotting and migration: The state of a VM—its memory, CPU, and disk—can be saved, cloned, or moved to another host
-
Consolidation: Multiple virtual servers can share one machine, increasing hardware utilization and reducing costs
-
Testing and recovery: Virtual machines can be paused, restored, or reset easily, supporting software development and disaster recovery
Security Implications
Virtualization offers strong isolation because the hypervisor mediates all access to hardware. A guest cannot normally read or modify another guest's memory or the hypervisor itself. However, vulnerabilities still exist:
-
VM escape: A compromised guest gains control over the hypervisor or host, usually by exploiting vulnerabilities in how the hypervisor emulates devices. This breaks isolation and gives the attacker access to all other virtual machines on the same host
-
Hypervisor vulnerabilities: Bugs in management interfaces or exposed APIs used for remote administration. Because the hypervisor controls all guest systems, these weaknesses are critical targets
-
Side-channel attacks: Exploit shared hardware resources to infer information by measuring timing or behavior
-
Shared-device risks: Multiple VMs using the same physical devices can allow information leakage or denial-of-service through poorly isolated drivers
Hypervisors are typically small and security-hardened, but their central role makes them high-value targets.
Containment Through Virtualization
From the perspective of containment, virtualization represents a deeper boundary. Process-level and container-level mechanisms rely on kernel enforcement. Virtualization adds a distinct kernel for each guest and isolates them with hardware-level checks. This separation makes virtualization the preferred choice for workloads requiring strong security guarantees, multi-tenant separation, or different operating systems.
In practice, many systems combine layers: containers run inside virtual machines, and those virtual machines run under a hypervisor on shared hardware. This layered approach provides both efficiency and assurance. Virtualization represents the deepest layer of software-based isolation—shifting enforcement from the kernel to the hardware level.
Key Takeaways
Containment operates at multiple layers, each providing different trade-offs between security, performance, and flexibility:
-
Application sandboxes restrict individual processes through filesystem isolation, system call filtering, or language-based environments
-
OS-level primitives (namespaces, cgroups, and capabilities) allow the kernel to isolate groups of processes, limit their resource consumption, and divide system privileges
-
Containers combine these primitives into lightweight, portable units for application deployment
-
Virtual machines provide the strongest isolation by emulating hardware and running separate operating systems
The progression from sandboxing to virtualization represents increasingly deeper isolation boundaries: from controlling what a process can see and do, to isolating groups of processes sharing a kernel, to separating entire operating systems with distinct kernels. Each layer builds on the principle of least privilege and defense in depth, restricting access and limiting the impact of compromise. Modern systems often combine multiple layers—running sandboxed applications in containers inside virtual machines—to balance efficiency with strong security guarantees.