Back to skill

Security audit

OpenClaw LinkedIn Poster Skill

Security checks for vulnerabilities and agentic risk

Overview

This LinkedIn posting skill is mostly purpose-aligned, but it handles posting authority and OAuth credentials in ways that need careful review before installation.

Install only if you are comfortable granting this skill authority to publish public LinkedIn posts, including organization posts when those scopes are granted. Prefer a version that uses a local or verifiably controlled OAuth callback, cryptographically random state, secure token storage, narrow scopes, and an explicit preview/confirmation step before every post.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
runner.cjs:92
Finding

OAuth Authorization Code Routed Through an Unverifiable Third-Party Callback Service

Content
View full analysis
setTimeout(resolve, 1000)); try { const response = await fetch(`${CALLBACK_SERVER}/api/token/${state}`); if (response.ok) { const data = await response.json(); code = data.code; break; } } catch (e) { // Continue polling } } if (!code) { throw new Error("Authorization timeout. Please try again."); } ``` ### Technical Analysis LinkedIn sends the OAuth authorization code to a hosted Railway service rather than directly to the local client. The client then retrieves the code by polling an endpoint keyed only by the OAuth state value. The callback server's implementation is not included in the audited project. Consequently, the audit cannot verify its authentication, authorization, code retenti ...[truncated 2261 chars]
Remediation
View remediation
/callback`. 2. Use PKCE with a cryptographically random `code_verifier` and corresponding `code_challenge` where supported. 3. If LinkedIn offers a device authorization flow appropriate to this client, prefer it over a shared callback relay. 4. If the relay must remain: - Publish and independently review its source code. - Authenticate callback retrieval rather than treating knowledge of state as authorization. - Bind each record to a separate high-entropy client secret or public-key proof. - Encrypt temporary authorization records. - Enforce short expiration and atomic one-time consumption. - Never log authorization codes or tokens. - Document ownership, hosting controls, retention policy, and incident response procedures. 5. Revoke existing authorizations and reauthorize users after replacing or hardening the callback infrastructure. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
runner.cjs:92
Finding

Predictable OAuth State Enables Authorization Session Confusion

Content
View full analysis
setTimeout(resolve, 1000)); try { const response = await fetch(`${CALLBACK_SERVER}/api/token/${state}`); if (response.ok) { const data = await response.json(); code = data.code; break; } } catch (e) { // Continue polling } } ``` ### Technical Analysis OAuth state is intended to be an unguessable value that binds an authorization response to the client session that initiated it. Here, state is only the current Unix time in milliseconds. A timestamp has low effective entropy when an attacker can estimate when the Skill was invoked. The client also uses knowledge of this value as the only visible means of retrieving a code from the shared callback service. No independent nonce, authenticated retrieval credential, local session binding, or explicit validation metadata is present in the audited client. This weakness is amplified by the use of a shared remote callback server. An attacker who can predict candidate timestamps may attempt to race or pre-populate callback records, enumerate nearby state values if the service permits it, or create an authorization response associated with the same state. ### Attack Path 1. The attacker estimates when the victim will invoke the LinkedIn Skill. 2. The attacker generates candidate state values from timestamps near that time. 3. The attacker initiat ...[truncated 1227 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
runner.cjs:57
Finding

LinkedIn Bearer Token Stored in Plaintext Without Explicit Restrictive Permissions

Content
View full analysis
data.expires_at) { console.log("Token expired."); return null; } return data.access_token; } catch (e) { console.error("Error reading token file:", e); return null; } } return null; } function saveToken(tokenData) { const data = { access_token: tokenData.access_token, expires_at: Date.now() + (tokenData.expires_in * 1000) }; fs.writeFileSync(TOKEN_FILE, JSON.stringify(data)); console.log(`\nToken saved to ${TOKEN_FILE}`); } ``` ### Technical Analysis The LinkedIn bearer token is serialized in plaintext to `.linkedin_token` inside the Skill directory. The write operation does not specify a restrictive file mode, and the load operation does not verify ownership, file type, or permissions. For newly created files, actual permissions depend on the process umask. On shared systems, permissive configuration may allow other users to read the token. If the file already exists, writing it does not correct pre-existing insecure permissions. Keeping a bearer credential in a project or Skill directory also increases the risk of accidental backup, packaging, synchronization, or source-control inclusion. A bearer token grants access based on possession; an attacker does not need the LinkedIn client secret to reuse a valid token against permitted API endpoints. ### Attack Path 1. The legitimate user completes OAuth. 2. `saveToken()` writes the access token in plaintext to `.linkedin_token`. 3. A local user, compromised process, backup service, synchr ...[truncated 978 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
runner.cjs:92
Finding

Shell Command Injection Through Unsanitized LinkedIn Client ID

Content
View full analysis
Remediation
View remediation
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 Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (17)

Credential Access

High
Category
Privilege Escalation
Confidence
91% confidence
Finding

The README states that OAuth tokens are exchanged via a pre-configured third-party callback server and then saved locally in .linkedin_token, which introduces credential-handling risk. A remote callback service can observe authorization data, and local token storage without documented protections may allow token theft and unauthorized posting to personal or company pages.

Content

Scanner excerpt · README.md (reported line 72)May include surrounding context.

md
## How It Works
1. When triggered, the script checks for a valid local token.
2. If no token exists (or it's expired), it starts the OAuth flow and prompts you to log in.
3. Once authorized, it exchanges the code for an access token via the secure callback server.
4. The token is saved locally (`.linkedin_token`) for future requests.
5. Your update is posted immediately to your profile.

Credential Access

High
Category
Privilege Escalation
Confidence
90% confidence
Finding

The skill states that it saves the access token locally for future use but provides no details about encryption, storage location, file permissions, or lifecycle controls. A locally stored bearer token can be stolen by malware, other local users, or backup/sync systems and then reused to post as the victim or associated organization.

Content

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

md
The first time you use the skill, it will:
1. Open a browser for LinkedIn authorization
2. Save the access token locally
3. Use the saved token for future posts

## Usage

Credential Access

High
Category
Privilege Escalation
Confidence
90% confidence
Finding

The skill obtains and persists a LinkedIn access token with broad social and organization scopes, creating a valuable credential that can be abused if the local token file is exposed. In this skill context, the token enables posting as the user and potentially to administered organizations, making compromise more impactful than a narrow read-only token.

Content

Scanner excerpt · runner.cjs (reported line 125)May include surrounding context.

js
throw new Error("Authorization timeout. Please try again.");
    }

    console.log("\n✅ Authorization received! Exchanging for access token...");
    
    const tokenData = await exchangeCodeForToken(code);
    saveToken(tokenData);

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The trigger phrase 'Post to LinkedIn' is broad and resembles a natural-language request an agent could match too eagerly, increasing the chance of unintended social media posting. Because this skill performs an external side effect on a real account, ambiguous activation raises the risk of accidental or prompt-influenced execution.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The manifest says the skill posts to the user's LinkedIn profile, but the documentation also enables posting to organization/company pages. This scope mismatch can cause users or orchestrators to grant or invoke broader publishing capabilities than expected, increasing the chance of unintended posting to business-owned accounts.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill describes posting functionality but does not clearly warn that invocation will publish content publicly to a user's profile or company page. Without an explicit disclosure and confirmation, users may trigger irreversible public posts through natural-language prompts without understanding the consequence.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The documentation normalizes use of a shared hosted OAuth callback server without warning users that OAuth authorization data will transit through third-party infrastructure. Even if implemented correctly, this introduces trust, interception, logging, and multi-tenant exposure risks that are especially sensitive because the skill also stores reusable access tokens.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The skill metadata says it posts to the user's LinkedIn profile, but the requested scopes and code also support posting to organizations/pages. That scope expansion can mislead users and reviewers about what authority is being granted, increasing the chance of unintended posts to managed organizations.

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 · runner.cjs (reported line 140)May include surrounding context.

js
params.append('client_id', process.env.LINKEDIN_CLIENT_ID);
    params.append('client_secret', process.env.LINKEDIN_CLIENT_SECRET);

    const response = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: params

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The organization lookup enumerates organizations where the user is an administrator, which is broader than a profile-posting skill needs. This expands data access and enables actions against organization accounts that users may not expect from the declared purpose.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
88% confidence
Finding

This call transmits the user's bearer token to LinkedIn to enumerate organizations they administer, a capability beyond the stated profile-posting purpose. In context, the danger is not the HTTPS request itself but the unnecessary collection and use of broader account/organization data.

Content

Scanner excerpt · runner.cjs (reported line 158)May include surrounding context.

js
console.log(`\nSearching for organization: "${orgName}"...`);
    
    // Fetch organizations where user is an admin
    const url = 'https://api.linkedin.com/v2/organizationalEntityAcls?q=roleAssignee&role=ADMINISTRATOR&state=APPROVED&projection=(elements*(organizationalTarget~(name)))';
    
    const response = await fetch(url, {
        headers: {

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% 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 · runner.cjs (reported line 204)May include surrounding context.

js
urn = await findOrganizationUrn(accessToken, orgName);
    } else {
        // Fallback to personal profile
        const profileResponse = await fetch('https://api.linkedin.com/v2/userinfo', {
            headers: { 'Authorization': `Bearer ${accessToken}` }
        });

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% 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 · runner.cjs (reported line 213)May include surrounding context.

js
urn = await findOrganizationUrn(accessToken, orgName);
    } else {
        // Fallback to personal profile
        const profileResponse = await fetch('https://api.linkedin.com/v2/userinfo', {
            headers: { 'Authorization': `Bearer ${accessToken}` }
        });

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% 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 · runner.cjs (reported line 240)May include surrounding context.

js
urn = await findOrganizationUrn(accessToken, orgName);
    } else {
        // Fallback to personal profile
        const profileResponse = await fetch('https://api.linkedin.com/v2/userinfo', {
            headers: { 'Authorization': `Bearer ${accessToken}` }
        });

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 · runner.cjs (reported line 240)May include surrounding context.

js
}
    };

    const postResponse = await fetch('https://api.linkedin.com/v2/ugcPosts', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${accessToken}`,

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
93% confidence
Finding

The manifest context says the skill posts updates to the user's LinkedIn profile via OAuth, which implies a personal-profile scope. However, the README explicitly advertises posting to company pages and organization pages as well, expanding behavior beyond the stated manifest description.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The skill invokes a platform shell opener through child_process.exec to open the OAuth URL in the user's browser. While OAuth itself is expected for LinkedIn posting, spawning a shell command is a stronger local-execution capability than the manifest description communicates.

Content

No source excerpt is available for this finding.