{"record":{"id":"67875b44f2182111","repo":"instructure/canvas-lms","slug":"invalid-url-in-xml-ampersands-must-be-escaped","errorCode":null,"errorMessage":"Invalid url in xml. Ampersands must be escaped.","messagePattern":"Invalid url in xml\\. Ampersands must be escaped\\.","errorType":"exception","errorClass":"CCImportError","httpStatus":null,"severity":"error","filePath":"lib/cc/importer/blti_converter.rb","lineNumber":139,"sourceCode":"      end\n      tool\n    end\n\n    def check_for_unescaped_url_properties(obj)\n      # Recursively look for properties named 'url'\n      case obj\n      when Hash\n        obj.select { |k, v| k.to_s == \"url\" && v.is_a?(String) }\n           .each_value { |v| check_for_unescaped_url(v) }\n        obj.each_value { |v| check_for_unescaped_url_properties(v) }\n      when Array\n        obj.each { |o| check_for_unescaped_url_properties(o) }\n      end\n    end\n\n    def check_for_unescaped_url(url)\n      if /(.*[^=]*\\?*=)[^&;]*=/.match?(url)\n        raise CCImportError, I18n.t(:invalid_url_in_xml, \"Invalid url in xml. Ampersands must be escaped.\")\n      end\n    end\n\n    def retrieve_and_convert_blti_url(url)\n      InstrumentTLSCiphers.without_tls_metrics do\n        response = CanvasHttp.get(url, redirect_limit: 10)\n        config_xml = response.body\n        convert_blti_xml(config_xml)\n      rescue Timeout::Error\n        raise CCImportError, I18n.t(:retrieve_timeout, \"could not retrieve configuration, the server response timed out\")\n      end\n    end\n\n    def get_custom_properties(node)\n      props = {}\n      node.children.each do |property|\n        next if property.name == \"text\"\n","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/cc/importer/blti_converter.rb#L121-L157","documentation":"check_for_unescaped_url scans URL property values in the imported BLTI config for query strings whose parameters are separated by a bare, unescaped ampersand. Because the surrounding XML would need &amp;, a raw '&' inside a URL signals malformed/unsafe markup, so a CCImportError is raised.","triggerScenarios":"check_for_unescaped_url_properties walks the parsed tool config and finds a property/element named 'url' (or similar) whose value matches /(.*[^=]*\\?*=)[^&;]*=/ — i.e. a URL with a query string containing an unescaped ampersand parameter separator.","commonSituations":"Vendors hand-generate config XML with raw '&' in launch/custom URLs; a config copied from a browser address bar keeps literal ampersands; template rendering did not HTML-escape the URL.","solutions":["Replace every raw & in URL values with &amp; in the configuration XML","Ask the tool vendor to regenerate the config with XML-escaped URLs","If generating XML programmatically, use a builder (Nokogiri::XML::Builder) so escaping is automatic"],"exampleFix":"# before\n<blti:launch_url>https://tool.example.com/launch?course_id=1&user_id=2</blti:launch_url>\n\n# after\n<blti:launch_url>https://tool.example.com/launch?course_id=1&amp;user_id=2</blti:launch_url>","handlingStrategy":"validation","validationCode":"raise CCImportError, 'unescaped ampersand in URL' if url.match?(/\\?[^\"<]*&/)","typeGuard":"def urls_escaped?(doc)\n  doc.xpath('//*[contains(local-name(), \"url\")]').all? do |n|\n    !n.text.match?(/(.*[^=]*\\?*=)[^&;]*=/)\n  end\nend","tryCatchPattern":"begin\n  tool = converter.convert_blti_xml(xml)\nrescue CCImportError => e\n  raise unless e.message.include?('Ampersands must be escaped')\n  Rails.logger.warn('Config XML contains unescaped ampersand in URL')\nend","preventionTips":["Always write &amp; instead of & in URL values inside XML","Generate config XML with Nokogiri::XML::Builder so escaping is automatic","Pre-validate vendor-supplied XML with the same regex before import"],"tags":["xml","lti","url","escaping","validation"],"backgroundTag":"invalid-url-format","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}