Skip to content

🔒 Sanitize CompanyUrl href output #21

Description

@SmolSoftBoi

🎯 What

SDSComXSL.xsl renders SupplierInformation/CompanyUrl in the generated HTML. The current markup uses a literal href="CompanyUrl", which appears to be a typo for a dynamic attribute value such as href="{CompanyUrl}".

If this is corrected without validation, untrusted SDS XML could supply a dangerous URL scheme and create a clickable XSS vector.

Vulnerable area:

<xsl:if test="CompanyUrl">
    <dt class="col-4">
        <h5>
            <xsl:value-of select="$Section1.3-CompanyUrl"/>
        </h5>
    </dt>
    <dd class="col-8">
        <a href="CompanyUrl" target="_blank"><xsl:value-of select="CompanyUrl"/></a>
    </dd>
</xsl:if>

⚠️ Risk

If a future fix changes this to href="{CompanyUrl}" directly, values such as the following could become executable or unsafe clickable links:

  • javascript:alert(1)
  • data:text/html,<script>alert(1)</script>
  • vbscript:...
  • //evil.example
  • whitespace-obfuscated URL schemes

Potential impact:

  • Cross-site scripting when users open transformed SDS HTML and click the company URL.
  • Phishing or redirection to malicious pages.
  • Reverse-tabnabbing risk because the link opens with target="_blank" and no rel protection.

Blast radius: any user viewing HTML generated from untrusted, external, or compromised SDS XML input.

🛡️ Proposed solution

Render CompanyUrl as a clickable link only when it is clearly safe:

  • Normalise whitespace.
  • Allow only http:// and https:// schemes.
  • Optionally convert plain domain-style values such as example.com to https://example.com.
  • Render unsafe values as escaped text instead of a link.
  • Add rel="noopener noreferrer" when using target="_blank".

Suggested replacement:

<xsl:variable name="companyUrl" select="normalize-space(CompanyUrl[1])"/>
<xsl:variable
    name="safeCompanyHref"
    select="
        if (matches(lower-case($companyUrl), '^https?://') and not(matches($companyUrl, '\s'))) then $companyUrl
        else if (matches($companyUrl, '^[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,}(/.*)?$')) then concat('https://', $companyUrl)
        else ''
    "
/>

<xsl:choose>
    <xsl:when test="$safeCompanyHref != ''">
        <a href="{$safeCompanyHref}" target="_blank" rel="noopener noreferrer">
            <xsl:value-of select="$companyUrl"/>
        </a>
    </xsl:when>
    <xsl:otherwise>
        <xsl:value-of select="$companyUrl"/>
    </xsl:otherwise>
</xsl:choose>

✅ Verification notes

Add or manually run checks confirming that unsafe schemes are not emitted as clickable href values:

  • javascript:alert(1) → escaped text only, no link.
  • data:text/html,<script>alert(1)</script> → escaped text only, no link.
  • //evil.example → escaped text only, no link.
  • https://example.com → clickable link.
  • http://example.com → clickable link.
  • example.com → clickable link as https://example.com if plain-domain support is retained.

Run the existing project checks after patching:

npm test

🧭 Implementation note

The XSLT is declared as version 3.0, so matches() and conditional expressions are appropriate for this stylesheet.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions