Skip to content

Lab 3: Software Supply Chain — Code Security

Warning

Due to a last minute change, the new url of the plaform is https://gitlab.tailccff78.ts.net/. Credentials are the same

For the same reason, refer to this other README to install the app.

Introduction

GitLab is a Git repository manager and DevOps platform with support for CI/CD pipelines.
You can explore GitLab’s full documentation here.

You will work with a Python-based application called cicdiaries, which is hosted in the cloud-security-lab GitLab repository.

To find it navigate to Projects > Member.

Note

Instructions on how to login to the platform should have been sent to you by email.

Task 1: Get Familiar with git, GitLab and the repository

Clone the Git repository and analyze it.

Follow the README.md and install the app.

Then create a new branch with the updated documentation on the python version: commit and push it. Finally create a merge request and merge the code into main.

Note

Only HTTPS is supported.

SSH access is disabled.

Warning

Some of you may not be able to install the app locally. This is because the developer did not clearly discuss the requirements of the app and specifically the python version that is 3.13.

Warning

Ensure you have disabled CI/CD pipelines for the project:

Go to Project's Settings >> General >> Visibility, project features, permissions

Expand the Repository section

Disable the CI/CD toggle

Then, click on Save Changes

Sign and Verify Commits

In this part, you will learn about signed and verified commits.

Task 2: Commit Changes as Someone Else

For this task, you have to leverage the fact that the Administrator does not have key verification enabled, allowing you to commit changes on their behalf.

As an example, create a new commit using the name Administrator and the email root. Use "Malicious commit." as the commit message.

Question 1

What happened when you tried to push your commit?

Did it succeed?

Who was the author?

Can you explain, in your own words, the security implications of this scenario?

Task 3: Enable Signed Commits with GPG

In this part, you will set up a GPG signing key and enable it in GitLab and in your local repository. Instructions on how to achieve this are available here. A successful setup should lead you to a state similar to the one shown in the figure below:

Signed commit
Signed commit example

Question 2

Now repeat the experiment from Task 2.

Do you notice any difference compared to the previous task?

Can you explain why?

How can the system verify the signature of a commit?

Tip

You can check signature information of commits using: git log --show-signature

Branch Protection

In this section, you will explore how branch protection rules can be used to enforce security practices in a GitLab repository. Branch protection helps prevent unauthorized changes and ensures that code changes go through proper review and verification processes.

Task 4: Configure Branch Protection Rules

  1. Navigate to your project in GitLab.
  2. Go to Settings > Repository > Protected branches.
  3. Protect the main branch by enabling the following options:
  4. Allow only maintainers to merge
  5. Prevent force push
  6. Allow no one push and merge

Question 3

How does enabling branch protection rules affect your ability to push changes directly to the main branch?

What are the benefits of enforcing such rules in a production environment?

Note

Remember that from now one, you can only merge changes to main via a merge request

Testing and Auditing

Code testing is an effective way to prevent errors in the source code.

Task 5: Fix a broken test

Use the command pytest (follow the instructions in the README file) to run the application tests. You should notice that one test is not passing.

Can you fix it?

Task 6: Test the Application for Security Issues

Testing is useful not only to verify the intended behaviour of an application but also to identify potential security issues. One of the most common threats in web applications is code or JavaScript injection. We can use testing to verify that our application is resilient to some of the most common injection attacks.

For instance, a malicious user or collaborator could inject malicious JavaScript or HTML into a blog post to, for example, trick visitors into clicking a malicious link. We can try to inject a simple script into the body of a blog post in a test and observe what happens.

Add the following test to app/tests/test_db.py, then answer the questions:

def test_blog_injection():
    malicious_body = '<script>alert("hacked")</script><b>blogpost</b>'

    post = BlogPost(
        id=3,
        title="Injection test",
        body=malicious_body,
        created_at=datetime(2025, 5, 10, 14, 30, 0)
    )

    serialized = post.serialize()

    # We want to escape all HTML tags in the body and title.
    assert "<script" not in serialized["body"]

Question 4

If you run the test, it will not pass.

What is the problem?

Which security property(ies) may be compromised by such an attack?

Can you find a countermeasure?

Task 7: SAST

The application uses the bandit tool which you are welcome to read and study. To run the security tests, execute the following command in the root folder:

bandit -r app -x venv,.pytest_cache,tests

Question 5

Did the tool find any security issues?

What does the output look like?

Can this be considered static or dynamic testing, and why?

Can you understand and fix the errors?

Credentials leak

The developer of the app once pushed an environment file which may contain important secrets under app/.env.

Question

Can you identify the commit in which that file was added?

(Optional) Fuzz testing

Fuzz testing consists in injecting random (or pseudo-random) inputs to a program. And our app needs a bit of fuzz testing too. As a main library, we are going to use Google's Atheris fuzzer.

You can install it by running pip install atheris

Note

MacOS users will need to install additional software as specified in the project's README.

You can also run:

brew install llvm               
CLANG_BIN=/usr/local/opt/llvm/bin/clang pip install atheris

Create a new file: app/fuzz_blog.py with the following content:

import atheris
import sys
import random
import re
from datetime import datetime
from atheris import FuzzedDataProvider

from website.models import BlogPost

SEED = 42

DANGEROUS_PATTERNS = [
    r"<\s*script",
    r"<\s*javascript:",
    r"<\s*iframe",
    r"<\s*img.*onerror",
    # TODO: find other interesting patterns
]

XSS_PAYLOADS = [b"Add Meaningful payloads"]

def custom_mutator(data: bytes, max_size: int, seed: int) -> bytes:
    random.seed(seed)

    rand = random.random()

    # ~20% of the times inject custom payloads
    if  rand < 0.2:
        return random.choice(XSS_PAYLOADS)

    # ~10% of the times return the original random data
    elif rand < 0.3:
        return bytes(data[:max_size])

    else: # mutate existing input
        data = bytearray(data)

        # TODO: Implement mutation logic.
        # Examples:
        # - Random byte flip
        # - Split XSS_PAYLOADS and append or prepend data
        # - Add opening or closing tags < >


        return bytes(data[:max_size])

def contains_xss(payload: str) -> bool: # This function checks for XSS
    payload_lower = payload.lower()
    return any(re.search(pattern, payload_lower) for pattern in DANGEROUS_PATTERNS)


def no_raw_html(output: str):
    # Avoid checking for benign cases like "<3" 
    if "<" in output:
        if not re.search(r"<[a-zA-Z!/]", output):
            return

        if "&lt;" not in output:
            raise AssertionError(f"Potential unescaped HTML: {repr(output)}")

@atheris.instrument_func
def TestHTMLInjection(data: bytes) -> None:
    fdp = FuzzedDataProvider(data)

    try:
        body = fdp.ConsumeUnicode(1000)
    except Exception:
        return 


    post = BlogPost(
        id=1,
        title="title",
        body=body,
        created_at=datetime(2025, 5, 10, 14, 30, 0)
    )

    serialized = post.serialize()


    # Validate all string fields, not just body
    for field, value in serialized.items():
        if not isinstance(value, str):
            continue

        # Pattern-based detection
        if contains_xss(value):
            raise AssertionError(f"XSS payload survived in field '{field}': {value}")

        # Raw html content detection
        no_raw_html(value)

def main():
    random.seed(SEED)

    args = sys.argv

    if not any(a.startswith("-seed=") for a in args):
        args = args[:1] + [f"-seed={SEED}"] + args[1:]

    atheris.Setup(
    args,
    TestHTMLInjection,
    custom_mutator=custom_mutator
)
    atheris.Fuzz()

if __name__ == "__main__":
    main()

To run the fuzzer go to the app folder and run python fuzz_blog.py -max_total_time=60

Question

Implement the custom_mutator function and add XSS_PAYLOADS. Then run the fuzzer. Do you find any error?

Try to fuzz the title field. What did you find?