T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/get_token.py:29- Finding
OAuth Helper Requests Unnecessary Organization Read and Administration Privileges
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/get_token.py:29OAuth Helper Requests Unnecessary Organization Read and Administration Privileges
scripts/get_token.py:137Access and Refresh Tokens Are Printed and Recommended for Plaintext Storage
scripts/get_token.py:52OAuth Error Parameters Are Reflected into HTML Without Escaping
{msg}{safe_msg}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.
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.
- **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)
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.
- **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)
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.
python scripts/get_token.py
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...
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
Access tokens last 60 days. Refresh tokens last 365. To mint a new access token without prompting the user again:
import requests
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
Access tokens last 60 days. Refresh tokens last 365. To mint a new access token without prompting the user again:
import requests
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
#!/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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
#!/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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
#!/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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
#!/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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
#!/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
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.
"""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.
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.
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)")
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
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.")
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.
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.
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.
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.
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.
- 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
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.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
import requests
resp = requests.post(
"https://www.linkedin.com/oauth/v2/accessToken",
data={
"grant_type": "refresh_token",
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
DEFAULT_API_VERSION = "202602" # February 2026. Bump deliberately.
API_BASE = "https://api.linkedin.com/rest"
MAX_COMMENTARY_LENGTH = 3000
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.
No suspicious patterns detected.