GitHub Security Advisory: GHSA-r48f-3986-4f9c
Package: fickling (PIP)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-184: Incomplete List of Disallowed Inputs
Vulnerable versions: <= 0.1.9
Patched in: 0.1.10

Summary: fickling modules linecache, difflib and gc are missing from the unsafe modules blocklist

Details:
# Our analysis

As stated in the [project's security policy](https://github.com/trailofbits/fickling/security/policy), we also don't consider `UnusedVariables` bypasses to be security issues. We added several unsafe modules mentioned by the reporter in advisory comments to the blocklist (https://github.com/trailofbits/fickling/commit/7f39d97258217ee2c21a1f5031d4a6d7343eb30d). 

# Original report

Title: UnusedVariables analysis bypass via BUILD opcode Arbitrary File Read through fickling.load()

### Summary
Two independent bugs in fickling's AST-based static analysis combine to allow a malicious pickle file to execute arbitrary stdlib function calls - including reading sensitive files - while check_safety() returns Severity.LIKELY_SAFE and fickling.load() completes without raising UnsafeFileError.

A server using fickling.load() as a security gate before deserializing untrusted pickle data (its documented use case) is fully bypassed. The attacker receives the contents of any file readable by the server process as the return value of fickling.load().

### Details
Interpreter.unused_assignments() does not scan the result assignment's RHS

File: fickling/fickle.py, Interpreter.unused_assignments(), ~line 1242

```python
for statement in self.module_body:
    if isinstance(statement, ast.Assign):
        if (
            len(statement.targets) == 1
            and isinstance(statement.targets[0], ast.Name)
            and statement.targets[0].id == "result"
        ):
            break
        ...
        statement = statement.value
    if statement is not None:
        for node in ast.walk(statement):
            if isinstance(node, ast.Name):
                used.add(node.id)
```

When the loop reaches result = _varN, it breaks immediately. The right-hand side of the result assignment is never walked for variable references. Any variable whose only reference is inside the result expression is therefore never added to the used set and is incorrectly flagged as unused - unless it also appears in an earlier non-assignment statement.

The BUILD opcode generates exactly such a non-assignment statement:

```python
# Build.run() generates:
_var4 = _var3                     # Assign  - _var3 added to used
_var4.__setstate__(_var2)         # Expr    - _var2 and _var4 added to used
```

This makes _var2 (the result of the dangerous call) appear in the used set via the __setstate__ expression, so UnusedVariables never flags it.

File: fickling/fickle.py, TupleThree.run(), and siblings

```python
def run(self, interpreter: Interpreter):
    top = interpreter.stack.pop()
    mid = interpreter.stack.pop()
    bot = interpreter.stack.pop()
    interpreter.stack.append(ast.Tuple((bot, mid, top), ast.Load()))
    #                                   ^^^^^^^^^^^^^^^^
    #                                   Python tuple, not list
```

Python's ast module requires repeated fields (such as Tuple.elts) to be lists. When elts is a Python tuple, ast.iter_child_nodes() does