I recently pulled an open-source MCP server that covered the functionality I needed.
Before using it with my account, I wanted to understand what it could access, where my credentials could travel, and how those credentials were stored.
The project was nanameru’s Substack MCP server. It provides tools for managing drafts, uploading images, and publishing through Substack’s internal API. The author had already done the integration work, and the README included security guidance and existing safeguards.
Reviewing the code revealed places where I wanted stronger controls. I made three sets of changes in my fork: tighter image ingestion, restrictions on authenticated requests, and safer session storage.
These changes address specific risks in a useful project. They also illustrate what I look for when evaluating software that operates with access to files or an authenticated account.
Image uploads needed tighter boundaries
The original upload tool checked file extensions and rejected paths under a list of sensitive directories, including SSH configuration, AWS credentials, and system directories. Those checks addressed an obvious concern: an assistant should not be able to upload sensitive local files through a tool intended for images.
But a blocked-folder list leaves considerable filesystem access available. Sensitive information can live in project directories, downloads, or other locations the list does not anticipate. A filename ending in .png also tells you little about its contents.
There was another route into the upload behavior. Images embedded in Markdown were processed by the underlying python-substack dependency, bypassing the upload tool’s validation. The original README explicitly acknowledged that limitation.
Strengthening the dedicated upload tool would leave that route open unless both used the same controls.
I replaced the local-file policy with explicitly configured asset directories. Local images are now disabled by default. To enable them, the operator sets SUBSTACK_IMAGE_ROOTS to the directories the connector should be allowed to read.
The implementation resolves paths before checking containment and rejects files outside those directories, including symlinks that resolve outside them. It requires an absolute path and a regular file, with a maximum input size of 10 MB.
It then checks the contents. Supported static images are decoded, checked against a 40-million-pixel limit, and converted into a fresh PNG. Rebuilding the image from its pixels removes metadata and trailing data rather than passing the original file through unchanged.
That introduces compatibility costs. Animated images, SVG, and HEIC require explicit conversion before use. Animation frames are never silently discarded.
For Markdown, I separated conversion from uploading. This line is a small but important part of the change:
# Conversion gets no API object, so it cannot read files or upload anything.
post.from_markdown(markdown, api=None)The connector checks the parsed image sources and prepares every image through the shared validation path before uploading any of them. A validation failure stops the operation rather than falling back to an unvalidated path.
Remote images needed their own restrictions. Downloads require HTTPS on port 443 and reject destinations resolving to non-public addresses. Redirect destinations undergo the same checks. The connection uses a validated IP address while retaining hostname verification for TLS.
Downloads also have size limits, a connection timeout, and bounded redirects. They use a separate connection without the authenticated account’s cookies.
The lesson is to trace every route into a sensitive capability. A validation function can be correct and still provide incomplete protection if another code path never calls it.
Authenticated requests needed explicit destinations
The next question was where the Substack session credential could go.
This connector uses a substack.sid cookie representing an authenticated session. I wanted destination rules enforced at the request boundary, because checking a publication URL during setup would not cover everything that happens later, particularly redirects.
The new authenticated session starts with one allowed host: substack.com. It retrieves the authenticated account’s publication list and verifies the requested publication appears there before adding its canonical or custom domain.
Every authenticated request must use HTTPS on port 443 and an exact allowed hostname. That validation also runs before redirected requests are sent. Lookalike domains, unrelated Substack subdomains, and unverified destinations are rejected.
This matters for more than cookies. A redirected request may carry private content in its body. Blocking an untrusted destination before sending helps contain both credentials and application data.
I also disabled implicit proxy and .netrc configuration for this session, kept TLS certificate verification enabled, and bounded redirects. Authenticated writes are not configured for automatic retries: an ambiguous response to a publishing operation should not casually trigger another attempt.
The implementation reuses the dependency’s endpoint methods through a restricted client, without modifying installed dependency files or globally changing other requests.
If a legitimate flow begins redirecting to another host, it will fail until that behavior is investigated and supported. That is an operational tradeoff, but it prevents the credential boundary from silently expanding.
For integrations like this, I want to understand how a destination becomes trusted. A plausible-looking URL supplied in configuration should not automatically receive account credentials.
Session credentials belonged in Keychain
Previously, the session token lived in a local configuration file protected by owner-only permissions. The client also used a temporary cookie file when initializing the API library, deleting it afterward.
Those safeguards were useful. For macOS, I wanted to reduce the places where the token was written to disk.
The default is now macOS Keychain. The configuration file records the publication URL and storage choice; the session token lives in the system credential backend.
Existing plaintext configurations migrate when loaded, but only after Keychain storage succeeds. If Keychain access is denied or unavailable, setup fails without silently switching to plaintext storage.
The authenticated client initializes cookies in memory, removing temporary cookie files from the main client and setup flows. A legacy helper remains for external script compatibility.
I also removed token fragments from setup output. Showing the beginning and end of a credential can seem convenient, but that output may reach screenshots, terminal logs, or support conversations. Setup now reports that the token is stored without displaying it. The credentials object also excludes the token from its default representation.
Hosted deployments still accept credentials through environment variables supplied by their secret-management system. Non-macOS installations retain protected file storage, which macOS users can also explicitly select. Configuration writes are atomic, with owner-only file and directory permissions.
Keychain improves storage for this installation. It does not make an authenticated session invulnerable.
What the checks establish
Each change includes regression tests around the controls it introduces.
The image tests cover invalid image contents, paths outside configured roots, symlink escapes, private network destinations, and Markdown validation failures that must prevent uploads. Transport tests check that untrusted requests and redirects are blocked, and publication hosts require membership verification. Credential tests use a mocked Keychain and synthetic tokens to check storage, migration, permissions, and redaction.
That coverage targets specific behaviors. It does not establish compatibility with every live Substack workflow. Login, custom domains, rendering, and scheduling still need account-specific integration checks.
The connector also retains its operating-system account’s privileges. Restricting its image tool does not restrict an agent with independent shell access. These changes strengthen the connector’s boundaries within that larger environment.
A Microcosm of Technical Due Diligence
This was a small repository review with a concrete outcome: three targeted hardening changes, regression coverage, and documented limitations.
The process is familiar from larger technical due diligence work. Understand what the software does. Trace the access it needs. Examine what it delegates to dependencies. Identify where assumptions should become enforced controls, then assess the consequences of changing them.
The project supplied the functionality I wanted. My work was examining whether its file access, network behavior, and credential handling met my requirements for using it.
Those questions also arise in inherited codebases, third-party integrations, and systems evaluated ahead of an investment or acquisition. The scope changes, but technical findings still need to inform practical decisions.
If you are evaluating a codebase and need to understand which risks deserve attention, that is part of the technical due diligence work I do. Get in touch.
