Security, Ethics & Professional Practices

Security Is Not Optional Engineering

Many engineers treat security as a feature to be added later — something the “security team” handles after the real engineering is done. This is exactly wrong. Security is a structural property of a system, like load-bearing capacity in a building. You can’t bolt it on after construction; it must be designed in from the start.

Consider: if your simulation platform processes proprietary structural designs for a client, a data breach doesn’t just violate a regulation — it exposes intellectual property that took years and millions of dollars to develop. If your bridge monitoring system can be tampered with, people could die. Security is not an IT concern. It is an engineering concern.

The Security Mindset

The security mindset means thinking about how a system can be misused, not just how it should be used. It means asking: “What happens if someone deliberately tries to break this?”

Threat Sources

Threat Source Description Example in Engineering Context
External attackers Hackers, competitors, nation-states Ransomware targeting simulation infrastructure
Insider threats Disgruntled employees, contractors Engineer exporting proprietary FEM models before leaving
Accidental exposure Misconfiguration, human error S3 bucket with client designs left publicly accessible
Supply chain attacks Compromised dependencies, tools Malicious code in a Python package used for post-processing

The STRIDE Threat Model

STRIDE is a systematic framework for identifying security threats. Developed at Microsoft, it categorizes threats into six types:

Threat Definition Engineering Example Mitigation
Spoofing Pretending to be someone else Attacker submits jobs as a legitimate client Strong authentication (MFA, JWT)
Tampering Modifying data or code Altering simulation input parameters in transit Integrity checks, signed payloads, HTTPS
Repudiation Denying an action was taken Client claims they never approved a design change Audit logging, tamper-proof logs
Information disclosure Exposing data to unauthorized parties Simulation results visible to competing firms Encryption at rest and in transit, access controls
Denial of service Making a system unavailable Flooding the job queue with fake simulations Rate limiting, input validation, auto-scaling
Elevation of privilege Gaining unauthorized access levels Intern account gains admin access to all projects Least privilege, role-based access control
Key insight: STRIDE is not a checklist to be completed once. It’s a thinking tool to be applied every time you design a new component, API endpoint, or data flow. The question is always: “For each STRIDE category, what could go wrong here?”

Core Security Principles

1. Least Privilege

Every component, user, and process should have the minimum permissions necessary to do its job — and no more.

Example: A simulation worker process needs to read input files from S3 and write results back. It does not need to delete files, modify IAM policies, or access the billing dashboard.

# BAD: Worker has full S3 access
{
    "Effect": "Allow",
    "Action": "s3:*",
    "Resource": "*"
}

# GOOD: Worker has minimum necessary permissions
{
    "Effect": "Allow",
    "Action": [
        "s3:GetObject"
    ],
    "Resource": "arn:aws:s3:::simulation-inputs/*"
},
{
    "Effect": "Allow",
    "Action": [
        "s3:PutObject"
    ],
    "Resource": "arn:aws:s3:::simulation-results/*"
}

Least privilege applies at every level: database users, API keys, IAM roles, file system permissions, network access. The principle is simple but requires discipline — it’s always easier to grant * and move on.

2. Input Validation: Never Trust User Input

This is the single most important security principle for developers. Every piece of data that enters your system from outside — user input, API calls, file uploads, query parameters — is a potential attack vector.

SQL Injection Example:

# BAD: String concatenation in SQL query
def get_simulation(sim_id):
    query = f"SELECT * FROM simulations WHERE id = '{sim_id}'"
    cursor.execute(query)
    return cursor.fetchone()

# If sim_id = "'; DROP TABLE simulations; --"
# The query becomes:
# SELECT * FROM simulations WHERE id = ''; DROP TABLE simulations; --'
# Your entire simulations table is deleted.

# GOOD: Parameterized query
def get_simulation(sim_id):
    query = "SELECT * FROM simulations WHERE id = %s"
    cursor.execute(query, (sim_id,))
    return cursor.fetchone()

# The database driver treats sim_id as DATA, not SQL code.
# No injection is possible regardless of what sim_id contains.

Common injection vectors:

Vector Attack Defense
SQL queries SQL injection Parameterized queries, ORM
HTML output Cross-site scripting (XSS) Output encoding, CSP headers
File paths Path traversal (../../etc/passwd) Validate resolved paths, chroot
Shell commands Command injection Avoid shell=True, use subprocess with list args
Deserialization Remote code execution Never deserialize untrusted data (avoid pickle)

Validation principles:

  • Validate on the server. Client-side validation is for UX, not security. It can be bypassed trivially.
  • Whitelist, don’t blacklist. Define what’s allowed, not what’s forbidden. Attackers are more creative than your blacklist.
  • Validate type, length, format, and range. A simulation ID should be a UUID. If it’s not, reject it immediately.
  • Fail closed. If validation fails, deny the request. Don’t try to “fix” the input.

3. Authentication and Authorization

Authentication (AuthN) answers: “Who are you?” Authorization (AuthZ) answers: “What are you allowed to do?” These are separate concerns and must be implemented separately.

import jwt
from functools import wraps
from flask import request, jsonify

def require_auth(allowed_roles):
    """Decorator that handles both authentication and authorization."""
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # --- Authentication: Who are you? ---
            token = request.headers.get("Authorization", "").replace("Bearer ", "")
            if not token:
                return jsonify({"error": "No token provided"}), 401

            try:
                payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
            except jwt.ExpiredSignatureError:
                return jsonify({"error": "Token expired"}), 401
            except jwt.InvalidTokenError:
                return jsonify({"error": "Invalid token"}), 401

            # --- Authorization: What can you do? ---
            user_role = payload.get("role", "")
            if user_role not in allowed_roles:
                return jsonify({"error": "Insufficient permissions"}), 403

            # Attach user info to request context
            request.current_user = payload
            return f(*args, **kwargs)
        return wrapper
    return decorator

# Usage:
@app.route("/api/simulations", methods=["POST"])
@require_auth(allowed_roles=["engineer", "admin"])
def create_simulation():
    """Only engineers and admins can create simulations."""
    # request.current_user is available here
    ...

@app.route("/api/admin/users", methods=["DELETE"])
@require_auth(allowed_roles=["admin"])
def delete_user():
    """Only admins can delete users."""
    ...

Notice the clear separation: a 401 response means “we don’t know who you are” (authentication failure), while a 403 means “we know who you are, but you’re not allowed to do this” (authorization failure). These are different problems requiring different solutions.

4. Secrets Management

Secrets — API keys, database passwords, encryption keys — are the most common source of security breaches. The rule is simple: secrets never go in code.

# BAD: Hardcoded secrets
DB_PASSWORD = "super_secret_password_123"
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"

# BETTER: Environment variables
import os
DB_PASSWORD = os.environ["DB_PASSWORD"]
AWS_ACCESS_KEY = os.environ["AWS_ACCESS_KEY_ID"]

# BEST: Secrets manager with rotation
import boto3

def get_secret(secret_name):
    """Retrieve a secret from AWS Secrets Manager."""
    client = boto3.client("secretsmanager")
    response = client.get_secret_value(SecretId=secret_name)
    return json.loads(response["SecretString"])

db_config = get_secret("production/database")
connection = psycopg2.connect(
    host=db_config["host"],
    password=db_config["password"],
    database=db_config["dbname"],
)
Tip: Add a pre-commit hook that scans for common secret patterns (API keys, passwords, tokens) and blocks the commit. Tools like detect-secrets and gitleaks automate this. One leaked secret in a public repository can be exploited within minutes — automated bots scan GitHub continuously for exposed credentials.

Ethics and Professional Responsibility

As engineers who write software, you bear responsibility beyond code correctness. Software systems affect people, and the decisions you make have consequences.

Honest Representation

When your simulation shows a bridge design is borderline, you don’t adjust the safety factor to make the numbers work. The same principle applies to software: don’t misrepresent system capabilities, performance, or reliability. If your system has a known failure mode, document it. If your model has limitations, state them.

Open-Source Licensing

Using open-source software comes with obligations. Licenses are not suggestions — they are legal contracts:

License Can you use in commercial product? Must you share your source code? Must you include the license?
MIT Yes No Yes
Apache 2.0 Yes No Yes (with NOTICE)
GPL v3 Yes, but… Yes, if distributed Yes
AGPL v3 Yes, but… Yes, even for SaaS Yes

If you include a GPL library in your simulation platform, you may be legally required to open-source your entire platform. This is not a hypothetical risk — it’s an active area of litigation.

Responsible AI Use

When using AI to generate code, you are responsible for that code. “The AI wrote it” is not a defense for a security vulnerability, a license violation, or incorrect results. Review AI-generated code with the same rigor you’d apply to code from a junior developer — capable but not always right.

Data Privacy

Engineering data often contains sensitive information: proprietary designs, client project details, personnel information. Regulations like GDPR, CCPA, and industry-specific standards (ITAR for defense, HIPAA for health-related structures) impose legal obligations. The key principles:

  • Collect only what you need. Don’t store data “just in case.”
  • Encrypt sensitive data at rest and in transit.
  • Define retention policies. Data you don’t have can’t be breached.
  • Implement access controls. Not everyone needs to see everything.
  • Plan for data subject requests. Can you export or delete a specific client’s data?

Exercise 17.1: STRIDE Threat Model for a Simulation API

Exercise: Apply the STRIDE framework to the following system:

System description: A REST API that allows engineering firms to submit structural simulation jobs. The system includes:

  • REST API with JWT authentication
  • S3 bucket for input/output files
  • PostgreSQL database for job metadata and user accounts
  • Email notifications when jobs complete

For each STRIDE category, identify:

  1. At least one specific threat (not generic — specific to this system)
  2. The impact if the threat is realized (data loss, financial, safety, reputational)
  3. A concrete mitigation (not “use encryption” — specify what, where, and how)

Present your analysis as a table with columns: STRIDE Category, Specific Threat, Impact, and Mitigation.

Stretch goal: Draw a data flow diagram of the system and mark the trust boundaries (points where data crosses from a trusted zone to an untrusted zone, or vice versa). Every trust boundary is a place where STRIDE analysis is most critical.

Quiz

Question: Your simulation API accepts a filename parameter to retrieve result files. A user sends the request:

GET /api/results?filename=../../../etc/passwd

What type of vulnerability is this, and what is the correct defense?

  1. SQL injection; use parameterized queries.
  2. Cross-site scripting; encode the output.
  3. Path traversal; validate that the resolved file path stays within the allowed results directory.
  4. Denial of service; rate-limit the API.
Answer

c) Path traversal; validate that the resolved file path stays within the allowed results directory.

The attacker is using ../ sequences to navigate up the directory tree and access files outside the intended results directory. The defense is to resolve the full path (using os.path.realpath() or equivalent) and verify that the result starts with the allowed base directory. For example:

import os

RESULTS_DIR = "/var/data/simulation-results"

def get_result_file(filename):
    requested_path = os.path.realpath(
        os.path.join(RESULTS_DIR, filename)
    )
    if not requested_path.startswith(RESULTS_DIR):
        raise PermissionError("Access denied: path traversal detected")
    return open(requested_path, "r")

This is not SQL injection (no database query), not XSS (no HTML output), and not DoS (the goal is data exfiltration, not service disruption).