Reporting Server shows me a SharePoint link I can’t access

When you setup SQL Server Reporting Services [SSRS] in SharePoint Integrated mode, browsing https://servername/ReportServer will show you a directory listing view of SharePoint Site Collections that is security trimmed to you. However, there are some downsides to this view.  Users can probe through the directory structure of your site that they have read access to, and users may see links to sites that you didn’t intend for them to see. 

In my test farm, when a user named BobSmith browses, he sees the following:

clip_image001[5]

If bobsmith clicks on the link for https://teams.jormanvmas.com, he is able to browse through the folder structure of the site collection.  If he ever finds a report [RDL file], he could click on the RDL file and the report would try to render. Users may also discover URLs that they didn’t know existed, and the person that owns the site may not want them to know the site exists.

For example, in the above output, BobSmith sees a link for https://teams.jormanvmas.com/sites/cqwp.  However, if he clicks that link he gets:

clip_image002

If he browses the URL directly in the browser, as opposed to going through Report Server, he gets the following:

clip_image003

This is odd, the Report Server page should return sites that BobSmith has permissions to, but for some reason, he’s unable to browse the URL returned.  To track down why this is occurring, I turned to the Debugging Tools for Windows.  I attached windbg to ReportingServicesService.exe, and broke in on managed exceptions [command : sxe clr].  This revealed that Report Services has an HttpHandler, that eventually calls OpenWeb for each site collection in the farm.

Microsoft.SharePoint.Library.SPRequest.OpenWeb(…)
Microsoft.SharePoint.SPWeb.InitWeb()
Microsoft.SharePoint.SPWeb.get_Description()
Microsoft.ReportingServices.SharePoint.Objects.RSSPImpWeb.get_Description()
Microsoft.ReportingServices.SharePoint.Utilities.CatalogItemUtilities.CreateCatalogItem(Microsoft.ReportingServices.SharePoint.ObjectModel.RSSPWeb)
Microsoft.ReportingServices.SharePoint.Server.SharePointDBInterface.InternalFindObjects(…)
Microsoft.ReportingServices.SharePoint.Server.SharePointDBInterface.FindObjectsNonRecursive(…)
Microsoft.ReportingServices.Library.ListChildrenAction.PerformActionNow()
Microsoft.ReportingServices.Library.RSSoapAction`1[[System.__Canon, mscorlib]].Execute()
Microsoft.ReportingServices.WebServer.ReportServiceHttpHandler.RenderFolder()
Microsoft.ReportingServices.WebServer.ReportServiceHttpHandler.RenderPageContent()
Microsoft.ReportingServices.WebServer.ReportServiceHttpHandler.RenderPage()
Microsoft.ReportingServices.WebServer.ReportServiceHttpHandler.ProcessRequest(System.Web.HttpContext)

I confirmed that when this call fails, the link will not render.  This means that BobSmith’s account is able to call OpenWeb on the Site, even though BobSmith is unable to browse the site. I then browsed the CQWP site as another account that has full access, and used the Check Permissions functionality to determine what permissions BobSmith has on the site.  This showed that BobSmith has Limited Access, which was granted via the Style Resource Readers group.

image

I then checked the Limited Access Permission Set and found that the following are granted:

  • View Application Pages  -  View forms, views, and application pages. Enumerate lists.
  • Browse User Information  -  View information about users of the Web site.
  • Use Remote Interfaces  -  Use SOAP, Web DAV, the Client Object Model or SharePoint Designer interfaces to access the Web site. 
  • Use Client Integration Features  -  Use features which launch client applications. Without this permission, users will have to work on documents locally and upload their changes. 
  • Open - Allows users to open a Web site, list, or folder in order to access items inside that container.

There we are, the link shows up because BobSmith has Open permissions, which is enough for Reporting Server to render the link.  I wanted to confirm why BobSmith was in the Style Resource Readers group. This group is added by the Publishing Features, and the group contains the NT Authority\Authenticated Users group.  For some information on Style Resource Readers group and the authenticated users group, see the following article https://technet.microsoft.com/en-us/library/cc262690.aspx

HTTPModule

From the above, we know that any site with the Publishing Features enabled will show up in the list of links when a user browses /ReportServer/, and if a user has read access, they can probe into the directory structure of your site.  There’s not an out of the box way to disable that view, so if you want to disable it, you’re looking at writing some code. I put together an HttpModule that will block incoming requests that will render that view.  The trick is that the Report Viewer control also makes requests to the /ReportServer URL to render reports, so you can’t block all requests.  I went with the following to determine if a request should be blocked:

  • Request has rs:Command QueryString set to ListChildren

OR

  • There is not an rs:Command and the request is to /ReportServer or /ReportServer/

The first scenario covers users sending a ListChildren command to the ReportServer.  The second scenario covers a user sending a GET to the ReportServer url in a browser, as well as users sending a POST request via PowerShell or some other language to the root.  These filters will still allow other rs:Command values as well as calls to the ASMX web services in the /ReportServer URL.

Here’s the code for a sample HttpModule.  There are 3 methods:

  • Init – hook up the BeginRequest event to your Application_BeginRequest method
  • Application_BeginRequest – This is where the URL is checked out
  • FailRequest – This clears and ends the response
  1: using System;
  2: using System.Web;
  3:  
  4: /// <summary>
  5: /// HttpModule that checks incoming URLs and blocks HTTP GET requests to /ReportServer or /ReportServer/. This
  6: /// prevents users from browsing the Report Server URL to click through links to try and find reports.
  7: /// 
  8: ///
  9: /// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
  10: /// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
  11: /// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
  12: /// PARTICULAR PURPOSE.
  13: /// </summary>
  14: /// 
  15:  
  16: namespace Sample.SharePoint.ReportServer
  17: {
  18:     public class ReportServerBlocker : IHttpModule
  19:     {
  20:         public void ReportServer()
  21:         {
  22:         }
  23:  
  24:         public void Dispose()
  25:         {
  26:             
  27:         }
  28:  
  29:         //When the module is initialized, hook the BeginRequest event
  30:         public void Init(HttpApplication context)
  31:         {
  32:             context.BeginRequest += new EventHandler(this.Application_BeginRequest);
  33:         }
  34:  
  35:         //When a new request comes in, check the URL. If the request is a GET request to /ReportServer or /ReportServer/, then clear and end the response.
  36:         private void Application_BeginRequest(Object source, EventArgs e)
  37:         {
  38:             HttpApplication application = (HttpApplication)source;
  39:             HttpContext context = application.Context;
  40:  
  41:             //Pull the HttpMethod and the URL from the request
  42:             string httpMethod = context.Request.HttpMethod.ToLower();
  43:             string path = context.Request.Url.AbsolutePath.ToLower();
  44:  
  45:             //Pull the rs:Command querystring
  46:             string command = context.Request.QueryString["rs:Command"];
  47:  
  48:             //if the rs:Command querystring has ListChildren, fail out the request
  49:             if (command != null)
  50:             {
  51:                 if (command.ToLower() == "listchildren")
  52:                 {
  53:                     FailRequest(context);
  54:                 }
  55:                 else
  56:                 {
  57:                     return;
  58:                 }
  59:             }
  60:             //if there is no querystring, and the request is to /reportserver or /reportserver/, fail the request
  61:             else if (command == null && (path.EndsWith("reportserver") || path.EndsWith("reportserver/")))
  62:             {
  63:                 FailRequest(context);
  64:             }            
  65:         }
  66:  
  67:         private void FailRequest(HttpContext context)
  68:         {
  69:             context.Response.Clear();
  70:             context.Response.End();
  71:         }
  72:  
  73:     }
  74: }

Once you build out the HttpModule, you add it to the <httpModules> section of the web.config for Report Server, then restart the SQL Server Reporting Services service.  By default, the web.config is found at :

C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\web.config

For example, if you build the above code into an assembly named Sample.SharePoint.ReportServer.dll, you would add the following to the <httpModules> section of the web.config [after the <clear/> tag]:

<add name="ReportViewerBlocker"
     type="Sample.SharePoint.ReportServer.ReportServerBlocker,
Sample.SharePoint.ReportServer,
Version=1.0.0.0, Culture=neutral,
PublicKeyToken=263da74b91424116" />