Feb 22, 2026·7 min read·4 visits
TeamCity failed to mask secrets when they were passed from one build to another as a dependency. If Build B used a password from Build A, it showed up as plain text in the logs. Fixed in version 2023.05.
A logic flaw in JetBrains TeamCity allowed sensitive 'password' type parameters to be written to build logs in plain text when passed between build configurations via dependencies. This vulnerability effectively bypasses the platform's secret masking mechanisms, turning build logs into a treasure trove for attackers with basic view permissions.
CI/CD pipelines are the modern equivalent of a confessional booth. We whisper our deepest, darkest secrets—AWS keys, database credentials, signing certificates—into them, trusting that the priest (in this case, the build server) will keep them safe. We rely on that comforting row of asterisks (******) in the console output to tell us that our secrets are being handled with the discretion of a Swiss banker.
JetBrains TeamCity is a behemoth in this space. It’s the engine room for thousands of enterprises. One of its core promises is that if you define a parameter as type password, it stays that way. It gets scrubbed from the UI, masked in the logs, and treated like radioactive material. But CVE-2023-34223 proves that even the best secret keepers get chatty when things get complicated.
This isn't a buffer overflow or a complex heap grooming attack. It’s a logic failure in how secrets travel. It turns out that while TeamCity was great at keeping a secret within a single room, it shouted it out loud the moment it had to pass that secret through the hallway to the next room. This vulnerability effectively unmasks the credentials that act as the keys to your entire infrastructure kingdom.
To understand this bug, you have to understand TeamCity's dependency model. Complex software delivery isn't just one script; it's a chain. Build A compiles the code, Build B runs the tests, and Build C deploys the artifact. TeamCity handles this via Snapshot Dependencies. A downstream build (Build B) can inherit properties from an upstream build (Build A).
The syntax for this is straightforward: %dep.BuildA.ParameterName%. When TeamCity orchestrates this, it resolves the variable from the upstream build context and injects it into the downstream execution context. The vulnerability, tracked internally as TW-81338, resides in this handover.
The masking subsystem—the code responsible for scanning output streams and replacing sensitive strings with ******—relies on metadata. It needs to know that a specific string is supposed to be a secret. In versions prior to 2023.05, when a password parameter was pulled across a dependency boundary, that metadata was seemingly lost or ignored during the logging phase. The value was resolved to its plain text form before the logger realized it was a 'password' type, resulting in your production database password sitting naked in teamcity-agent.log.
While we don't have the raw Java patch diffs, we can reconstruct the failure accurately through the behavior of the build agent's variable resolution logic. In a secure implementation, a parameter defined as password (let's call it secure_param) is wrapped in a SecureString object or equivalent abstraction that overrides toString() to return asterisks.
The Vulnerable Flow:
db_password as type password. value: Hunter2!.%dep.BuildA.db_password%.# Conceptually, the config looks like this:
object: BuildType
id: DeployConfig
dependency:
id: BuildConfig
parameters:
# The fatal reference
deploy_key = %dep.BuildConfig.secure_password%When the agent spins up the build runner for DeployConfig, it prints the environment variables and parameters for debugging purposes. Because the 'password' attribute was attached to the definition in BuildConfig, the context in DeployConfig treated it as just another string coming from a dependency. The result in the log file:
[Step 1/1] Starting: /opt/deploy_script.sh
[Step 1/1] in directory: /opt/buildagent/work/...
[Step 1/1] Parameter 'deploy_key' = 'Hunter2!' <-- OOPS.The Fix (2023.05):
The patch involves ensuring that the password property type is transitive. When a dependency parameter is resolved, the system now checks the source definition's type. If the source says password, the destination logger is instructed to add that value to the blacklist of strings to be masked, regardless of where it came from.
Exploiting this requires no hacking tools, no Metasploit, and no buffer overflows. It requires a web browser and a pair of eyes. The attack vector is strictly "Insider Threat" or "Compromised User."
The Scenario:
Imagine you are a contractor or a junior developer. You have Project Viewer rights. You can't see the 'Administration' tab where the secrets are defined, and you can't edit the build configurations to echo secrets. However, you can view build logs to debug why a build failed.
KeyVault_Fetch).[10:42:15] : [Resolving artifact dependencies]
[10:42:15] : ... resolved %dep.AuthService.api_token% to "ey...[FULL JWT OR API KEY]..."Since the masking failed, the token is right there. You copy it, paste it into your local terminal, and now you have production access. It is the digital equivalent of finding the CEO's password written on a sticky note under their keyboard, except the keyboard is published to the company intranet.
The CVSS score of 4.3 is deceptively low. It assumes that if you are already inside the network (AV:N) and have an account (PR:L), the damage is limited (C:L). I fundamentally disagree with this assessment in a real-world context.
In modern DevOps, the CI/CD server is the Skeleton Key. It has write access to your git repositories, push access to your Docker registries, and deployment access to your Kubernetes clusters and AWS accounts. A single leaked credential here—say, a AWS_SECRET_ACCESS_KEY or a GITHUB_TOKEN with write permissions—allows for immediate lateral movement.
Real-World Consequences:
While the vulnerability requires authentication, in large organizations, "read access to logs" is often granted to hundreds of developers. Any one of them—or anyone who compromises one of their accounts—can harvest these credentials.
If you are running any version of TeamCity prior to 2023.05, you are vulnerable. The primary fix is simple: Upgrade. JetBrains has patched the resolution logic to ensure dependency parameters carry their 'password' status with them.
Immediate Remediation Steps:
> [!WARNING]
> Do not forget the Agent Logs. The leak happens on the build agent as well as the server. Ensure you purge teamcity-agent.log on all your build nodes, not just the central server UI logs.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
TeamCity JetBrains | < 2023.05 | 2023.05 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-532 |
| Attack Vector | Network |
| CVSS | 4.3 (Medium) |
| EPSS Score | 0.00004 |
| Exploit Status | None (No Public PoC) |
| Patch Version | 2023.05 |
Insertion of Sensitive Information into Log File