
Seal Security example — vulnerable pip app (PyYAML CVE-2020-14343) remediated to sealed versions; GitHub Actions + Jenkins integration
A minimal, intentionally vulnerable Flask application used to demonstrate, end‑to‑end, how Seal Security remediates a known CVE by replacing a vulnerable dependency with a sealed (backported, drop‑in) version — with no change to your declared requirements or your code.
It is designed as a front‑to‑back smoke test for the Seal CLI in CI/CD: run the app, trigger a real exploit, run Seal, and watch the same exploit get blocked.
| Ecosystem | Python / pip |
| Vulnerable package | PyYAML==5.1 |
| CVE | CVE‑2020‑14343 — yaml.load / FullLoader deserialization → arbitrary code execution |
| Sealed (fixed) version | pyyaml 5.1+sp1 from Seal's PyPI registry |
| Integration | Seal CLI as one build step — shown for both GitHub Actions and Jenkins |
The welcome page takes a name and parses it through PyYAML's default loader:
parsed = yaml.load(name) # PyYAML 5.1 → unsafe FullLoader (CVE-2020-14343)
On PyYAML 5.1, yaml.load() without an explicit SafeLoader uses the FullLoader, which can
construct arbitrary Python objects from untrusted input. An attacker submits a YAML payload that
evaluates arbitrary Python on the server.
Normal request
/?name=alice → Welcome, alice!
Exploit request — pass this YAML as name (already URL‑encoded in the workflow logs):
!!python/object/apply:tuple [!!python/object/apply:map [!!python/name:eval , ["__import__('subprocess').check_output(['id']).decode()"]]]
The vulnerable PyYAML deserializes and runs the payload, the app shows a “You've been pwned” page, and the server is then killed (a few seconds later, so the page delivers first). Reload and the app is gone — over ngrok you'll see an “endpoint offline” page.
.
├── app.py # the vulnerable Flask app
├── requirements.txt # declares PyYAML==5.1
├── Jenkinsfile # example Jenkins (Groovy) pipeline with the Seal stage
└── .github/workflows/
├── build-and-run.yml # run + expose the app for browser testing
└── seal-security.yml # run Seal remediation, then start the app
Seal is SaaS, Seal‑hosted — nothing is installed inside your environment, and all traffic is outbound HTTPS on TCP 443 only. To run the remediation you need:
| Secret / credential |
|---|
Configure these in Settings → Secrets and variables → Actions (GitHub) or Manage Jenkins → Credentials (Jenkins). Never commit tokens to the repo.
Allowlist these Seal hosts for outbound 443:
app.sealsecurity.io, authorization.sealsecurity.io, cli.sealsecurity.io, and — for
sealed pip packages — pypi.sealsecurity.io. The CLI binary is downloaded from
github.com / objects.githubusercontent.com.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python app.py # → http://localhost:5000
Open http://localhost:5000/?name=alice (works), then submit the exploit payload above as the
name — the app shows "You've been pwned" and the server is killed a few seconds later.
The Seal CLI runs as one extra step, after pip install and before packaging. It scans the
resolved dependencies and rewrites the vulnerable ones to their sealed versions, using remote
fix mode (policy is managed centrally in the Seal UI).
Uses seal-community/cli-action:
- uses: seal-community/cli-action@latest
with:
mode: fix
fix_mode: remote
token: ${{ secrets.SEAL_TOKEN }}
target: requirements.txt # the manifest for this ecosystem
Run it via Actions → “Seal Security Remediation” → Run workflow. See
.github/workflows/seal-security.yml.
A single added stage, after install and before packaging. See Jenkinsfile:
stage('Seal') {
steps {
sh '''
curl -fsSL https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest -o seal
chmod +x seal
./seal fix --mode remote "$SEAL_MANIFEST" # SEAL_MANIFEST=requirements.txt
'''
}
}
SEAL_TOKEN comes from the seal-token Jenkins credential; set SEAL_PROJECT to your Seal
Project ID.
After seal fix, the vulnerable dependency resolves to a sealed build from Seal's PyPI
registry — same package, security fix backported:
| Dependency | Before | After (sealed) |
|---|---|---|
| PyYAML | 5.1 | 5.1+sp1 |
A sealed version is the same package with the security fix backported — a drop‑in replacement, no code changes and no major‑version upgrade.
Re‑run the exploit against the remediated app. The sealed PyYAML refuses to construct the
malicious objects, so yaml.load raises instead of executing the payload, and the app responds
with “Invalid input — payload rejected.” Normal names still work.
seal fix at the specific manifest — requirements.txt for pip. For a repo with
multiple manifests/lock files, run one seal fix per manifest.That's the whole integration — one stage, outbound‑only, no changes to application code.
| Used for |
|---|
| Where it goes |
|---|
| Seal token | Authenticating the Seal CLI | GitHub Actions secret SEAL_TOKEN / Jenkins "Secret text" credential seal-token |
| ngrok token (optional) | Exposing the running app to a browser for testing | GitHub Actions secret NGROK_TOKEN |