Back to skill

Security audit

Linkedin Page Publisher

Security checks for vulnerabilities and agentic risk

Overview

This LinkedIn publishing skill is mostly coherent, but it asks for broader account permissions than its posting purpose needs and exposes long-lived OAuth tokens in plaintext.

Review before installing. Use this only with a LinkedIn app and administrator account you intend to authorize for Company Page posting. Do not run the token helper in logged, shared, CI, or screen-shared environments; avoid storing tokens in shell profiles or committed .env files. Prefer reducing the OAuth scopes to only w_organization_social, store secrets in a proper secret manager, and use --dry-run plus explicit approval before any live post.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/get_token.py:29
Finding

OAuth Helper Requests Unnecessary Organization Read and Administration Privileges

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/get_token.py:137
Finding

Access and Refresh Tokens Are Printed and Recommended for Plaintext Storage

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/get_token.py:52
Finding

OAuth Error Parameters Are Reflected into HTML Without Escaping

Content
View full analysis
OAuth error
text
{msg}
".encode()) _CallbackHandler.result["error"] = msg elif "code" in params and "state" in params: ``` ### Technical Analysis The callback handler obtains `error_description` or `error` directly from the request query string and interpolates the value into an HTML response without HTML escaping. The HTTP server listens only on `127.0.0.1` and is active for a limited period, which significantly reduces exposure. Nevertheless, while the OAuth helper is running, a crafted request to the callback can supply HTML markup or script content that the browser interprets in the localhost response origin. The handler validates OAuth state later for successful authorization codes, but the error branch does not require or validate a state value before reflecting the supplied content. State validation would not replace output encoding, but requiring the expected state would further reduce the callback’s acceptance of unsolicited requests. ### Attack Path 1. The victim starts `scripts/get_token.py`, opening a local listener on `127.0.0.1:8765`. 2. During the five-minute callback window, the victim visits or is redirected by an attacker-controlled page. 3. The attacker causes the browser to navigate to a URL such as: `http://localhost:8765/callback?error=x&error_description=` 4. The local callback handler inserts the supplied value into an HTML response without escaping it. 5. The victim’s browser renders the injected markup and may execute active content, subject to browser security controls. ### Impact Assessment The injected content executes or renders in the local callback page ra ...[truncated 532 chars]
Remediation
View remediation
OAuth error
text
{safe_msg}
".encode("utf-8") ) ``` 2. Prefer returning a fixed generic browser message and printing detailed error information only to the terminal. 3. Require and validate the expected OAuth state for error callbacks as well as successful callbacks. 4. Add a restrictive Content Security Policy, for example: ```python self.send_header( "Content-Security-Policy", "default-src 'none'; style-src 'none'; frame-ancestors 'none'; base-uri 'none'" ) ``` 5. Add `X-Content-Type-Options: nosniff` and an explicit UTF-8 content type. 6. Add tests using HTML tags, event handlers, quotes, and encoded payloads to verify that callback parameters are rendered only as text. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (23)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared purpose centers on publishing posts, but the skill also includes OAuth token acquisition, browser sign-in, a local callback server, and credential storage. That broader behavior materially expands the attack surface because a user or orchestrator may invoke a publishing skill without realizing it can collect and persist long-lived credentials.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
78% confidence
Finding

By highlighting long-lived access and refresh tokens and noting they are saved, the skill implies handling of durable credentials with elevated organizational posting capability. In this context, insufficient disclosure and likely plaintext/local persistence make compromise more damaging than a generic token mention because refresh tokens extend attacker access well beyond a single session.

Content

Scanner excerpt · SKILL.md (reported line 107)May include surrounding context.

md
- **Personal token limit**: roughly 100 calls/day/member. Respect this when building schedulers.
- **Scope required**: `w_organization_social`. The authenticating user must be an admin of the Company Page — being an employee is not enough.
- **Post character limit**: 3,000. The API returns HTTP 422 if exceeded. The library checks locally before calling so the failure is cheaper.
- **Access token lifetime**: 60 days. Refresh tokens last 365 days and can be used to mint new access tokens without re-prompting the user. `get_token.py` saves both.

## What LinkedIn's API cannot do (as of 2026)

Credential Access

High
Category
Privilege Escalation
Confidence
78% confidence
Finding

By highlighting long-lived access and refresh tokens and noting they are saved, the skill implies handling of durable credentials with elevated organizational posting capability. In this context, insufficient disclosure and likely plaintext/local persistence make compromise more damaging than a generic token mention because refresh tokens extend attacker access well beyond a single session.

Content

Scanner excerpt · SKILL.md (reported line 107)May include surrounding context.

md
- **Personal token limit**: roughly 100 calls/day/member. Respect this when building schedulers.
- **Scope required**: `w_organization_social`. The authenticating user must be an admin of the Company Page — being an employee is not enough.
- **Post character limit**: 3,000. The API returns HTTP 422 if exceeded. The library checks locally before calling so the failure is cheaper.
- **Access token lifetime**: 60 days. Refresh tokens last 365 days and can be used to mint new access tokens without re-prompting the user. `get_token.py` saves both.

## What LinkedIn's API cannot do (as of 2026)

Credential Access

High
Category
Privilege Escalation
Confidence
98% confidence
Finding

The setup flow normalizes exposing both access and refresh tokens in terminal output. These are bearer credentials; anyone who obtains them can act as the authorized LinkedIn app/user, and the refresh token especially can be used to mint new access tokens for an extended period.

Content

Scanner excerpt · references/setup.md (reported line 56)May include surrounding context.

python scripts/get_token.py

text

The helper opens a browser, you sign in as the Company Page admin, approve the scopes, and the script prints the access token and refresh token to stdout. Copy them into your shell profile or `.env`:

```bash
export LINKEDIN_ACCESS_TOKEN=AQV...

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/setup.md (reported line 74)May include surrounding context.

Refreshing the access token later

Access tokens last 60 days. Refresh tokens last 365. To mint a new access token without prompting the user again:

python
import requests

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/setup.md (reported line 74)May include surrounding context.

Refreshing the access token later

Access tokens last 60 days. Refresh tokens last 365. To mint a new access token without prompting the user again:

python
import requests

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 8)May include surrounding context.

md
#!/usr/bin/env python3
"""One-time OAuth 2.0 helper to get an access token for a LinkedIn Company Page.

Runs a tiny local HTTP server to catch the redirect, exchanges the code, and
prints both the access token and the refresh token. You only run this once

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 23)May include surrounding context.

md
#!/usr/bin/env python3
"""One-time OAuth 2.0 helper to get an access token for a LinkedIn Company Page.

Runs a tiny local HTTP server to catch the redirect, exchanges the code, and
prints both the access token and the refresh token. You only run this once

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/setup.md (reported line 72)May include surrounding context.

md
#!/usr/bin/env python3
"""One-time OAuth 2.0 helper to get an access token for a LinkedIn Company Page.

Runs a tiny local HTTP server to catch the redirect, exchanges the code, and
prints both the access token and the refresh token. You only run this once

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/get_token.py (reported line 2)May include surrounding context.

python
#!/usr/bin/env python3
"""One-time OAuth 2.0 helper to get an access token for a LinkedIn Company Page.

Runs a tiny local HTTP server to catch the redirect, exchanges the code, and
prints both the access token and the refresh token. You only run this once

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/get_token.py (reported line 6)May include surrounding context.

python
#!/usr/bin/env python3
"""One-time OAuth 2.0 helper to get an access token for a LinkedIn Company Page.

Runs a tiny local HTTP server to catch the redirect, exchanges the code, and
prints both the access token and the refresh token. You only run this once

Credential Access

High
Category
Privilege Escalation
Confidence
93% confidence
Finding

This script explicitly prints both the access token and refresh token to stdout after exchange. Even though the tool is intended to help a legitimate operator bootstrap OAuth, emitting bearer credentials to the terminal can leak them via shell history capture, terminal logging, CI logs, remote session transcripts, or shoulder-surfing. In this skill context, the refresh token is especially sensitive because it can enable long-term access to a LinkedIn Company Page.

Content

Scanner excerpt · scripts/get_token.py (reported line 5)May include surrounding context.

python
"""One-time OAuth 2.0 helper to get an access token for a LinkedIn Company Page.

Runs a tiny local HTTP server to catch the redirect, exchanges the code, and
prints both the access token and the refresh token. You only run this once
per environment — after that, the access token lives in env vars and the
refresh token renews it for the next year.

Credential Access

High
Category
Privilege Escalation
Confidence
96% confidence
Finding

The code prints LINKEDIN_ACCESS_TOKEN and potentially LINKEDIN_REFRESH_TOKEN in plaintext to stdout. These are bearer secrets; anyone with access to the terminal output or captured logs can reuse them to act as the LinkedIn application/user, potentially publishing or administering organization content until expiry or revocation. Because this skill targets company-page publishing and requests organization scopes, misuse could lead to unauthorized posts or persistent account access.

Content

Scanner excerpt · scripts/get_token.py (reported line 146)May include surrounding context.

python
print(f"LINKEDIN_ACCESS_TOKEN={body['access_token']}")
    if "refresh_token" in body:
        print(f"LINKEDIN_REFRESH_TOKEN={body['refresh_token']}")
    print(f"\nAccess token expires in {body.get('expires_in', '?')} seconds.")
    print("Add these to your shell profile or .env file.\n")
    print("Next step: find your Company Page's numeric ID at")
    print("  https://www.linkedin.com/company/<slug>/admin/  (visible in the URL)")

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/get_token.py (reported line 147)May include surrounding context.

python
if "refresh_token" in body:
        print(f"LINKEDIN_REFRESH_TOKEN={body['refresh_token']}")
    print(f"\nAccess token expires in {body.get('expires_in', '?')} seconds.")
    print("Add these to your shell profile or .env file.\n")
    print("Next step: find your Company Page's numeric ID at")
    print("  https://www.linkedin.com/company/<slug>/admin/  (visible in the URL)")
    print("and export it as LINKEDIN_ORG_ID.")

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
86% confidence
Finding

The skill declares no explicit tool/permission boundaries even though it clearly relies on environment variables and network access. That omission makes it easier for an agent framework to invoke credentialed network actions without clear review or sandbox expectations, increasing the chance of unintended token use or live posting.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The invocation language is very broad and encourages use for many loosely related LinkedIn requests, which raises the chance of over-triggering a skill that can perform authenticated, real-world posting actions. In agent systems, broad matching can cause accidental execution in contexts where the user only wanted advice, not publication or token-handling flows.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The documentation mentions obtaining and saving access/refresh tokens but does not prominently warn that these are sensitive credentials requiring secure handling. Users may expose tokens in logs, shells, screenshots, CI variables, or shared environments, enabling account misuse against the company page.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The usage examples describe publishing commands but do not clearly emphasize that non-dry-run execution creates immediate live posts on a public-facing LinkedIn Company Page. Without an explicit warning, users or agent orchestrators may unintentionally publish test, debug, or malformed content to a real audience.

Content

No source excerpt is available for this finding.

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
75% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

Scanner excerpt · references/setup.md (reported line 28)May include surrounding context.

md
- Sometimes a call with a LinkedIn rep.
4. Approval typically takes a few days to a couple of weeks. Plan accordingly. During review, the app can still authenticate but `POST /rest/posts` will 403.

If you just want to test without approval, you can use `w_member_social` to post to a personal profile instead. But this skill is scoped to Company Pages, so that path needs changes to both `client.py` (use `urn:li:person:...`) and the CLI (accept a person URN instead of an org ID).

## 3. Configure Auth settings

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The guide explicitly instructs users to print OAuth access and refresh tokens to stdout and then copy them into a shell profile or .env file without any warning about secret handling, terminal history, process logging, shared shells, or source-control exposure. Because refresh tokens grant long-lived reauthentication, accidental disclosure can enable persistent unauthorized posting to the LinkedIn page.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · references/setup.md (reported line 78)May include surrounding context.

python
import requests
resp = requests.post(
    "https://www.linkedin.com/oauth/v2/accessToken",
    data={
        "grant_type": "refresh_token",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/lib/client.py (reported line 24)May include surrounding context.

python
DEFAULT_API_VERSION = "202602"  # February 2026. Bump deliberately.
API_BASE = "https://api.linkedin.com/rest"
MAX_COMMENTARY_LENGTH = 3000

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

This code sends data over the network via requests.post, requests.get, and requests.put, including authenticated API calls and raw uploads. While the module docstrings describe the API mechanics, there is no user-facing confirmation, logging, or explicit warning here that content and credentials-related context will be transmitted to LinkedIn.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.